@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 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
- export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, type UseProbatMetricsReturn, fetchDecision, sendMetric, useProbatMetrics };
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
- export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, type UseProbatMetricsReturn, fetchDecision, sendMetric, useProbatMetrics };
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/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
+ 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 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]);
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: 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
- [id, variantKey, instanceId, customerId]
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 || !resolved) return;
390
+ if (!trackImpression) return;
398
391
  impressionSent.current = false;
399
392
  const pageKey = getPageKey();
400
- const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);
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, impressionEvent, eventProps);
412
- if (debug) console.log(`[probat] Impression sent (no IO) for "${id}"`);
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, impressionEvent, eventProps);
426
- if (debug) console.log(`[probat] Impression sent for "${id}"`);
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
- resolved,
444
- id,
436
+ experimentId,
445
437
  variantKey,
446
438
  instanceId,
447
439
  host,
448
- impressionEvent,
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, clickEvent, {
449
+ sendMetric(host, clickEventName, {
458
450
  ...eventProps,
459
451
  ...meta
460
452
  });
461
453
  if (debug) {
462
- console.log(`[probat] Click tracked for "${id}"`, meta);
454
+ console.log(`[probat] Click tracked for "${experimentId}"`, meta);
463
455
  }
464
456
  },
465
- [trackClick, host, clickEvent, eventProps, id, debug]
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: containerRef,
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