@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 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
- export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, type UseProbatMetricsReturn, fetchDecision, sendMetric, useProbatMetrics };
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
- export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, type UseProbatMetricsReturn, fetchDecision, sendMetric, useProbatMetrics };
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/components/Experiment.tsx
295
- var ASSIGNMENT_PREFIX = "probat:assignment:";
296
- function readAssignment(id) {
297
- if (typeof window === "undefined") return null;
298
- try {
299
- const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);
300
- if (!raw) return null;
301
- const parsed = JSON.parse(raw);
302
- return parsed.variantKey ?? null;
303
- } catch {
304
- return null;
305
- }
306
- }
307
- function writeAssignment(id, variantKey) {
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
+ 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 trackImpression = track?.impression !== false;
328
- const trackClick = track?.primaryClick !== false;
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 (debug && resolved) {
379
- console.log(`[probat] Experiment "${id}" \u2192 variant "${variantKey}"`, {
380
- instanceId,
381
- pageKey: getPageKey()
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
- }, [debug, id, variantKey, resolved, instanceId]);
389
+ }, [isCustomerMode, resolvedCustomerId, experimentId, debug]);
385
390
  const eventProps = React3.useMemo(
386
391
  () => ({
387
- experiment_id: id,
388
- variant_key: variantKey,
392
+ experiment_id: experimentId,
393
+ ...variantKey ? { variant_key: variantKey } : {},
389
394
  component_instance_id: instanceId,
390
- ...customerId ? { distinct_id: customerId } : {}
395
+ ...resolvedCustomerId ? { distinct_id: resolvedCustomerId } : {}
391
396
  }),
392
- [id, variantKey, instanceId, customerId]
397
+ [experimentId, variantKey, instanceId, resolvedCustomerId]
393
398
  );
394
- const containerRef = React3.useRef(null);
395
- const impressionSent = React3.useRef(false);
399
+ const dedupeVariant = variantKey ?? resolvedCustomerId ?? "__anon__";
396
400
  React3.useEffect(() => {
397
- if (!trackImpression || !resolved) return;
401
+ if (!trackImpression) return;
398
402
  impressionSent.current = false;
399
403
  const pageKey = getPageKey();
400
- const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);
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, impressionEvent, eventProps);
412
- if (debug) console.log(`[probat] Impression sent (no IO) for "${id}"`);
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, impressionEvent, eventProps);
426
- if (debug) console.log(`[probat] Impression sent for "${id}"`);
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
- resolved,
444
- id,
445
- variantKey,
447
+ experimentId,
448
+ dedupeVariant,
446
449
  instanceId,
447
450
  host,
448
- impressionEvent,
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, clickEvent, {
460
+ sendMetric(host, clickEventName, {
458
461
  ...eventProps,
459
462
  ...meta
460
463
  });
461
464
  if (debug) {
462
- console.log(`[probat] Click tracked for "${id}"`, meta);
465
+ console.log(`[probat] Click tracked for "${experimentId}"`, meta);
463
466
  }
464
467
  },
465
- [trackClick, host, clickEvent, eventProps, id, debug]
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: containerRef,
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