@probat/react 0.4.1 → 0.4.3
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 +117 -7
- package/dist/index.d.ts +117 -7
- package/dist/index.js +185 -109
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +182 -110
- 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 +682 -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 +213 -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,91 @@ 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 UseTrackBaseOptions {
|
|
68
|
+
/** Experiment identifier */
|
|
69
|
+
experimentId: string;
|
|
70
|
+
/** Stable instance id when multiple instances of the same experiment exist on a page */
|
|
71
|
+
componentInstanceId?: string;
|
|
72
|
+
/** Auto-track impressions (default true) */
|
|
73
|
+
impression?: boolean;
|
|
74
|
+
/** Auto-track clicks (default true) */
|
|
75
|
+
click?: boolean;
|
|
76
|
+
/** Custom impression event name (default "$experiment_exposure") */
|
|
77
|
+
impressionEventName?: string;
|
|
78
|
+
/** Custom click event name (default "$experiment_click") */
|
|
79
|
+
clickEventName?: string;
|
|
80
|
+
/** Log events to console */
|
|
81
|
+
debug?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** Explicit mode: pass the variant key directly */
|
|
84
|
+
interface UseTrackExplicitOptions extends UseTrackBaseOptions {
|
|
85
|
+
/** The variant key to attach to events */
|
|
86
|
+
variantKey: string;
|
|
87
|
+
customerId?: undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Customer mode: backend resolves variant from assignment table */
|
|
90
|
+
interface UseTrackCustomerOptions extends UseTrackBaseOptions {
|
|
91
|
+
variantKey?: undefined;
|
|
92
|
+
/** Customer ID to resolve variant server-side. Falls back to provider's customerId. */
|
|
93
|
+
customerId?: string;
|
|
94
|
+
}
|
|
95
|
+
type UseTrackOptions = UseTrackExplicitOptions | UseTrackCustomerOptions;
|
|
96
|
+
/**
|
|
97
|
+
* Attaches impression and click tracking to a DOM element via a ref.
|
|
98
|
+
*
|
|
99
|
+
* Two modes:
|
|
100
|
+
* - **Explicit**: pass `variantKey` directly — stamped on every event.
|
|
101
|
+
* - **Customer**: omit `variantKey` — backend resolves it from the assignment table
|
|
102
|
+
* using `(experimentId, customerId)`.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* // Explicit mode
|
|
107
|
+
* const trackRef = useTrack({ experimentId: "pricing", variantKey: "ai_v1" });
|
|
108
|
+
*
|
|
109
|
+
* // Customer mode (backend resolves variant)
|
|
110
|
+
* const trackRef = useTrack({ experimentId: "pricing", customerId: "user_123" });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
declare function useTrack(options: UseTrackOptions): React.RefObject<HTMLElement | null>;
|
|
114
|
+
|
|
115
|
+
type TrackProps = UseTrackOptions & {
|
|
116
|
+
children: React$1.ReactNode;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Wrapper component that attaches impression and click tracking to its children.
|
|
120
|
+
* Alternative to the `useTrack` hook when you prefer a component over a ref.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* <Track experimentId="pricing" variantKey="ai_v1">
|
|
125
|
+
* <PricingCard />
|
|
126
|
+
* </Track>
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function Track({ children, ...trackOptions }: TrackProps): React$1.JSX.Element;
|
|
130
|
+
|
|
131
|
+
interface UseExperimentOptions {
|
|
132
|
+
/** Behavior when assignment fetch fails: "control" (default) renders control, "suspend" throws */
|
|
133
|
+
fallback?: "control" | "suspend";
|
|
134
|
+
/** Log decisions to console */
|
|
135
|
+
debug?: boolean;
|
|
136
|
+
}
|
|
137
|
+
interface UseExperimentReturn {
|
|
138
|
+
/** The resolved variant key (e.g. "control", "ai_v1") */
|
|
139
|
+
variantKey: string;
|
|
140
|
+
/** Whether the assignment has been resolved (bootstrap, cache, or fetch) */
|
|
141
|
+
resolved: boolean;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolves the variant assignment for an experiment.
|
|
145
|
+
* No tracking — use `useTrack` or `<Track>` to observe impressions/clicks.
|
|
146
|
+
*
|
|
147
|
+
* Priority: bootstrap > localStorage cache > fetchDecision.
|
|
148
|
+
*/
|
|
149
|
+
declare function useExperiment(id: string, options?: UseExperimentOptions): UseExperimentReturn;
|
|
66
150
|
|
|
67
151
|
interface UseProbatMetricsReturn {
|
|
68
152
|
/**
|
|
@@ -102,4 +186,30 @@ declare function fetchDecision(host: string, experimentId: string, distinctId: s
|
|
|
102
186
|
*/
|
|
103
187
|
declare function sendMetric(host: string, event: string, properties: Record<string, unknown>): void;
|
|
104
188
|
|
|
105
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Factory that creates a typed context + hook pair for passing a variantKey
|
|
191
|
+
* between components in different files without prop-drilling.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* // experiment-context.ts
|
|
196
|
+
* export const { ExperimentProvider, useVariantKey } = createExperimentContext("pricing-test");
|
|
197
|
+
*
|
|
198
|
+
* // Parent.tsx
|
|
199
|
+
* const { variantKey } = useExperiment("pricing-test");
|
|
200
|
+
* <ExperimentProvider value={variantKey}><Child /></ExperimentProvider>
|
|
201
|
+
*
|
|
202
|
+
* // Child.tsx (different file)
|
|
203
|
+
* const variantKey = useVariantKey();
|
|
204
|
+
* const trackRef = useTrack({ experimentId: "pricing-test", variantKey });
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
declare function createExperimentContext(experimentId: string): {
|
|
208
|
+
ExperimentProvider: ({ value, children, }: {
|
|
209
|
+
value: string;
|
|
210
|
+
children: ReactNode;
|
|
211
|
+
}) => React$1.FunctionComponentElement<React$1.ProviderProps<string | null>>;
|
|
212
|
+
useVariantKey: () => string;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, Track, type TrackProps, type UseExperimentOptions, type UseExperimentReturn, type UseProbatMetricsReturn, type UseTrackCustomerOptions, type UseTrackExplicitOptions, 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,91 @@ 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 UseTrackBaseOptions {
|
|
68
|
+
/** Experiment identifier */
|
|
69
|
+
experimentId: string;
|
|
70
|
+
/** Stable instance id when multiple instances of the same experiment exist on a page */
|
|
71
|
+
componentInstanceId?: string;
|
|
72
|
+
/** Auto-track impressions (default true) */
|
|
73
|
+
impression?: boolean;
|
|
74
|
+
/** Auto-track clicks (default true) */
|
|
75
|
+
click?: boolean;
|
|
76
|
+
/** Custom impression event name (default "$experiment_exposure") */
|
|
77
|
+
impressionEventName?: string;
|
|
78
|
+
/** Custom click event name (default "$experiment_click") */
|
|
79
|
+
clickEventName?: string;
|
|
80
|
+
/** Log events to console */
|
|
81
|
+
debug?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** Explicit mode: pass the variant key directly */
|
|
84
|
+
interface UseTrackExplicitOptions extends UseTrackBaseOptions {
|
|
85
|
+
/** The variant key to attach to events */
|
|
86
|
+
variantKey: string;
|
|
87
|
+
customerId?: undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Customer mode: backend resolves variant from assignment table */
|
|
90
|
+
interface UseTrackCustomerOptions extends UseTrackBaseOptions {
|
|
91
|
+
variantKey?: undefined;
|
|
92
|
+
/** Customer ID to resolve variant server-side. Falls back to provider's customerId. */
|
|
93
|
+
customerId?: string;
|
|
94
|
+
}
|
|
95
|
+
type UseTrackOptions = UseTrackExplicitOptions | UseTrackCustomerOptions;
|
|
96
|
+
/**
|
|
97
|
+
* Attaches impression and click tracking to a DOM element via a ref.
|
|
98
|
+
*
|
|
99
|
+
* Two modes:
|
|
100
|
+
* - **Explicit**: pass `variantKey` directly — stamped on every event.
|
|
101
|
+
* - **Customer**: omit `variantKey` — backend resolves it from the assignment table
|
|
102
|
+
* using `(experimentId, customerId)`.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* // Explicit mode
|
|
107
|
+
* const trackRef = useTrack({ experimentId: "pricing", variantKey: "ai_v1" });
|
|
108
|
+
*
|
|
109
|
+
* // Customer mode (backend resolves variant)
|
|
110
|
+
* const trackRef = useTrack({ experimentId: "pricing", customerId: "user_123" });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
declare function useTrack(options: UseTrackOptions): React.RefObject<HTMLElement | null>;
|
|
114
|
+
|
|
115
|
+
type TrackProps = UseTrackOptions & {
|
|
116
|
+
children: React$1.ReactNode;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Wrapper component that attaches impression and click tracking to its children.
|
|
120
|
+
* Alternative to the `useTrack` hook when you prefer a component over a ref.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```tsx
|
|
124
|
+
* <Track experimentId="pricing" variantKey="ai_v1">
|
|
125
|
+
* <PricingCard />
|
|
126
|
+
* </Track>
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function Track({ children, ...trackOptions }: TrackProps): React$1.JSX.Element;
|
|
130
|
+
|
|
131
|
+
interface UseExperimentOptions {
|
|
132
|
+
/** Behavior when assignment fetch fails: "control" (default) renders control, "suspend" throws */
|
|
133
|
+
fallback?: "control" | "suspend";
|
|
134
|
+
/** Log decisions to console */
|
|
135
|
+
debug?: boolean;
|
|
136
|
+
}
|
|
137
|
+
interface UseExperimentReturn {
|
|
138
|
+
/** The resolved variant key (e.g. "control", "ai_v1") */
|
|
139
|
+
variantKey: string;
|
|
140
|
+
/** Whether the assignment has been resolved (bootstrap, cache, or fetch) */
|
|
141
|
+
resolved: boolean;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Resolves the variant assignment for an experiment.
|
|
145
|
+
* No tracking — use `useTrack` or `<Track>` to observe impressions/clicks.
|
|
146
|
+
*
|
|
147
|
+
* Priority: bootstrap > localStorage cache > fetchDecision.
|
|
148
|
+
*/
|
|
149
|
+
declare function useExperiment(id: string, options?: UseExperimentOptions): UseExperimentReturn;
|
|
66
150
|
|
|
67
151
|
interface UseProbatMetricsReturn {
|
|
68
152
|
/**
|
|
@@ -102,4 +186,30 @@ declare function fetchDecision(host: string, experimentId: string, distinctId: s
|
|
|
102
186
|
*/
|
|
103
187
|
declare function sendMetric(host: string, event: string, properties: Record<string, unknown>): void;
|
|
104
188
|
|
|
105
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Factory that creates a typed context + hook pair for passing a variantKey
|
|
191
|
+
* between components in different files without prop-drilling.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* // experiment-context.ts
|
|
196
|
+
* export const { ExperimentProvider, useVariantKey } = createExperimentContext("pricing-test");
|
|
197
|
+
*
|
|
198
|
+
* // Parent.tsx
|
|
199
|
+
* const { variantKey } = useExperiment("pricing-test");
|
|
200
|
+
* <ExperimentProvider value={variantKey}><Child /></ExperimentProvider>
|
|
201
|
+
*
|
|
202
|
+
* // Child.tsx (different file)
|
|
203
|
+
* const variantKey = useVariantKey();
|
|
204
|
+
* const trackRef = useTrack({ experimentId: "pricing-test", variantKey });
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
declare function createExperimentContext(experimentId: string): {
|
|
208
|
+
ExperimentProvider: ({ value, children, }: {
|
|
209
|
+
value: string;
|
|
210
|
+
children: ReactNode;
|
|
211
|
+
}) => React$1.FunctionComponentElement<React$1.ProviderProps<string | null>>;
|
|
212
|
+
useVariantKey: () => string;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, Track, type TrackProps, type UseExperimentOptions, type UseExperimentReturn, type UseProbatMetricsReturn, type UseTrackCustomerOptions, type UseTrackExplicitOptions, 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,48 @@ 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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
componentInstanceId,
|
|
368
|
+
impression: trackImpression = true,
|
|
369
|
+
click: trackClick = true,
|
|
370
|
+
impressionEventName = "$experiment_exposure",
|
|
371
|
+
clickEventName = "$experiment_click",
|
|
372
|
+
debug = false
|
|
373
|
+
} = options;
|
|
374
|
+
const variantKey = options.variantKey ?? void 0;
|
|
375
|
+
const explicitCustomerId = "customerId" in options ? options.customerId : void 0;
|
|
376
|
+
const { host, customerId: providerCustomerId } = useProbatContext();
|
|
377
|
+
const resolvedCustomerId = explicitCustomerId ?? providerCustomerId;
|
|
378
|
+
const isCustomerMode = !variantKey;
|
|
379
|
+
const autoInstanceId = useStableInstanceId(experimentId);
|
|
326
380
|
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]);
|
|
381
|
+
const containerRef = React3.useRef(null);
|
|
382
|
+
const impressionSent = React3.useRef(false);
|
|
377
383
|
React3.useEffect(() => {
|
|
378
|
-
if (
|
|
379
|
-
console.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
});
|
|
384
|
+
if (isCustomerMode && !resolvedCustomerId && debug) {
|
|
385
|
+
console.warn(
|
|
386
|
+
`[probat] useTrack called without variantKey and no customerId available for "${experimentId}". Events will have no variant attribution.`
|
|
387
|
+
);
|
|
383
388
|
}
|
|
384
|
-
}, [
|
|
389
|
+
}, [isCustomerMode, resolvedCustomerId, experimentId, debug]);
|
|
385
390
|
const eventProps = React3.useMemo(
|
|
386
391
|
() => ({
|
|
387
|
-
experiment_id:
|
|
388
|
-
variant_key: variantKey,
|
|
392
|
+
experiment_id: experimentId,
|
|
393
|
+
...variantKey ? { variant_key: variantKey } : {},
|
|
389
394
|
component_instance_id: instanceId,
|
|
390
|
-
...
|
|
395
|
+
...resolvedCustomerId ? { distinct_id: resolvedCustomerId } : {}
|
|
391
396
|
}),
|
|
392
|
-
[
|
|
397
|
+
[experimentId, variantKey, instanceId, resolvedCustomerId]
|
|
393
398
|
);
|
|
394
|
-
const
|
|
395
|
-
const impressionSent = React3.useRef(false);
|
|
399
|
+
const dedupeVariant = variantKey ?? resolvedCustomerId ?? "__anon__";
|
|
396
400
|
React3.useEffect(() => {
|
|
397
|
-
if (!trackImpression
|
|
401
|
+
if (!trackImpression) return;
|
|
398
402
|
impressionSent.current = false;
|
|
399
403
|
const pageKey = getPageKey();
|
|
400
|
-
const dedupeKey = makeDedupeKey(
|
|
404
|
+
const dedupeKey = makeDedupeKey(experimentId, dedupeVariant, instanceId, pageKey);
|
|
401
405
|
if (hasSeen(dedupeKey)) {
|
|
402
406
|
impressionSent.current = true;
|
|
403
407
|
return;
|
|
@@ -408,8 +412,8 @@ function Experiment({
|
|
|
408
412
|
if (!impressionSent.current) {
|
|
409
413
|
impressionSent.current = true;
|
|
410
414
|
markSeen(dedupeKey);
|
|
411
|
-
sendMetric(host,
|
|
412
|
-
if (debug) console.log(`[probat] Impression sent (no IO) for "${
|
|
415
|
+
sendMetric(host, impressionEventName, eventProps);
|
|
416
|
+
if (debug) console.log(`[probat] Impression sent (no IO) for "${experimentId}"`);
|
|
413
417
|
}
|
|
414
418
|
return;
|
|
415
419
|
}
|
|
@@ -422,8 +426,8 @@ function Experiment({
|
|
|
422
426
|
if (impressionSent.current) return;
|
|
423
427
|
impressionSent.current = true;
|
|
424
428
|
markSeen(dedupeKey);
|
|
425
|
-
sendMetric(host,
|
|
426
|
-
if (debug) console.log(`[probat] Impression sent for "${
|
|
429
|
+
sendMetric(host, impressionEventName, eventProps);
|
|
430
|
+
if (debug) console.log(`[probat] Impression sent for "${experimentId}"`);
|
|
427
431
|
observer.disconnect();
|
|
428
432
|
}, 250);
|
|
429
433
|
} else if (timer) {
|
|
@@ -440,12 +444,11 @@ function Experiment({
|
|
|
440
444
|
};
|
|
441
445
|
}, [
|
|
442
446
|
trackImpression,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
variantKey,
|
|
447
|
+
experimentId,
|
|
448
|
+
dedupeVariant,
|
|
446
449
|
instanceId,
|
|
447
450
|
host,
|
|
448
|
-
|
|
451
|
+
impressionEventName,
|
|
449
452
|
eventProps,
|
|
450
453
|
debug
|
|
451
454
|
]);
|
|
@@ -454,22 +457,59 @@ function Experiment({
|
|
|
454
457
|
if (!trackClick) return;
|
|
455
458
|
const meta = extractClickMeta(e.target);
|
|
456
459
|
if (!meta) return;
|
|
457
|
-
sendMetric(host,
|
|
460
|
+
sendMetric(host, clickEventName, {
|
|
458
461
|
...eventProps,
|
|
459
462
|
...meta
|
|
460
463
|
});
|
|
461
464
|
if (debug) {
|
|
462
|
-
console.log(`[probat] Click tracked for "${
|
|
465
|
+
console.log(`[probat] Click tracked for "${experimentId}"`, meta);
|
|
463
466
|
}
|
|
464
467
|
},
|
|
465
|
-
[trackClick, host,
|
|
468
|
+
[trackClick, host, clickEventName, eventProps, experimentId, debug]
|
|
466
469
|
);
|
|
470
|
+
React3.useEffect(() => {
|
|
471
|
+
const el = containerRef.current;
|
|
472
|
+
if (!el || !trackClick) return;
|
|
473
|
+
el.addEventListener("click", handleClick);
|
|
474
|
+
return () => {
|
|
475
|
+
el.removeEventListener("click", handleClick);
|
|
476
|
+
};
|
|
477
|
+
}, [handleClick, trackClick]);
|
|
478
|
+
return containerRef;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/components/Experiment.tsx
|
|
482
|
+
function Experiment({
|
|
483
|
+
id,
|
|
484
|
+
control,
|
|
485
|
+
variants,
|
|
486
|
+
track,
|
|
487
|
+
componentInstanceId,
|
|
488
|
+
fallback = "control",
|
|
489
|
+
debug = false
|
|
490
|
+
}) {
|
|
491
|
+
const { variantKey: rawKey, resolved } = useExperiment(id, { fallback, debug });
|
|
492
|
+
const variantKey = rawKey === "control" || rawKey in variants ? rawKey : "control";
|
|
493
|
+
if (debug && rawKey !== variantKey) {
|
|
494
|
+
console.warn(
|
|
495
|
+
`[probat] Unknown variant "${rawKey}" for experiment "${id}", falling back to control`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
const trackRef = useTrack({
|
|
499
|
+
experimentId: id,
|
|
500
|
+
variantKey,
|
|
501
|
+
componentInstanceId,
|
|
502
|
+
impression: resolved ? track?.impression !== false : false,
|
|
503
|
+
click: track?.primaryClick !== false,
|
|
504
|
+
impressionEventName: track?.impressionEventName,
|
|
505
|
+
clickEventName: track?.clickEventName,
|
|
506
|
+
debug
|
|
507
|
+
});
|
|
467
508
|
const content = variantKey === "control" || !(variantKey in variants) ? control : variants[variantKey];
|
|
468
509
|
return /* @__PURE__ */ React3__default.default.createElement(
|
|
469
510
|
"div",
|
|
470
511
|
{
|
|
471
|
-
ref:
|
|
472
|
-
onClick: handleClick,
|
|
512
|
+
ref: trackRef,
|
|
473
513
|
"data-probat-experiment": id,
|
|
474
514
|
"data-probat-variant": variantKey,
|
|
475
515
|
style: {
|
|
@@ -483,6 +523,19 @@ function Experiment({
|
|
|
483
523
|
content
|
|
484
524
|
);
|
|
485
525
|
}
|
|
526
|
+
function Track({ children, ...trackOptions }) {
|
|
527
|
+
const trackRef = useTrack(trackOptions);
|
|
528
|
+
return /* @__PURE__ */ React3__default.default.createElement(
|
|
529
|
+
"div",
|
|
530
|
+
{
|
|
531
|
+
ref: trackRef,
|
|
532
|
+
"data-probat-track": trackOptions.experimentId,
|
|
533
|
+
"data-probat-variant": trackOptions.variantKey ?? "server-resolved",
|
|
534
|
+
style: { display: "contents" }
|
|
535
|
+
},
|
|
536
|
+
children
|
|
537
|
+
);
|
|
538
|
+
}
|
|
486
539
|
function useProbatMetrics() {
|
|
487
540
|
const { host, customerId } = useProbatContext();
|
|
488
541
|
const capture = React3.useCallback(
|
|
@@ -496,11 +549,34 @@ function useProbatMetrics() {
|
|
|
496
549
|
);
|
|
497
550
|
return { capture };
|
|
498
551
|
}
|
|
552
|
+
function createExperimentContext(experimentId) {
|
|
553
|
+
const Ctx = React3.createContext(null);
|
|
554
|
+
function ExperimentProvider({
|
|
555
|
+
value,
|
|
556
|
+
children
|
|
557
|
+
}) {
|
|
558
|
+
return React3__default.default.createElement(Ctx.Provider, { value }, children);
|
|
559
|
+
}
|
|
560
|
+
function useVariantKey() {
|
|
561
|
+
const v = React3.useContext(Ctx);
|
|
562
|
+
if (v === null) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
`useVariantKey() must be used inside <ExperimentProvider> for "${experimentId}"`
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return v;
|
|
568
|
+
}
|
|
569
|
+
return { ExperimentProvider, useVariantKey };
|
|
570
|
+
}
|
|
499
571
|
|
|
500
572
|
exports.Experiment = Experiment;
|
|
501
573
|
exports.ProbatProviderClient = ProbatProviderClient;
|
|
574
|
+
exports.Track = Track;
|
|
575
|
+
exports.createExperimentContext = createExperimentContext;
|
|
502
576
|
exports.fetchDecision = fetchDecision;
|
|
503
577
|
exports.sendMetric = sendMetric;
|
|
578
|
+
exports.useExperiment = useExperiment;
|
|
504
579
|
exports.useProbatMetrics = useProbatMetrics;
|
|
580
|
+
exports.useTrack = useTrack;
|
|
505
581
|
//# sourceMappingURL=index.js.map
|
|
506
582
|
//# sourceMappingURL=index.js.map
|