@cascayd/experiment 0.3.22 → 0.3.24

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.
@@ -1,4 +1,4 @@
1
- import type { ReactElement } from 'react';
1
+ import type { ReactElement } from "react";
2
2
  export type ExperimentProps = {
3
3
  id: string;
4
4
  children: React.ReactNode;
@@ -1,28 +1,30 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react';
2
- import { getVariantStatus } from '../http';
3
- import { record } from '../client';
4
- import { getOrCreateSession, persistVariantChoice, readVariantChoice } from '../cookies';
5
- import { buildWeights, chooseByWeightDeterministic, resolveVariant } from '../assignment';
6
- const sentImpressions = new Set();
1
+ "use client";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { getVariantStatus } from "../http";
4
+ import { record } from "../client";
5
+ import { getOrCreateSession, persistVariantChoice, readVariantChoice } from "../cookies";
6
+ import { buildWeights, chooseByWeightDeterministic, resolveVariant } from "../assignment";
7
7
  export function Experiment({ id, children }) {
8
- const [active, setActive] = useState(null);
9
- const hasRunRef = useRef(false);
10
8
  const childrenArray = useMemo(() => {
11
9
  const arr = [];
12
10
  for (const child of [].concat(children)) {
13
- if (!child || child.type?.displayName !== 'CascaydVariant')
11
+ if (!child || child.type?.displayName !== "CascaydVariant")
14
12
  continue;
15
13
  arr.push({ id: child.props.id, weight: child.props.weight, element: child });
16
14
  }
17
15
  return arr;
18
16
  }, [children]);
17
+ // Always render control initially (deterministic for SSR + hydration)
18
+ const initial = useMemo(() => resolveVariant("control", childrenArray), [childrenArray]);
19
+ const [active, setActive] = useState(initial);
20
+ const hasRunRef = useRef(false);
21
+ const sentImpressionsRef = useRef(new Set());
19
22
  useEffect(() => {
20
23
  if (hasRunRef.current)
21
24
  return;
22
25
  hasRunRef.current = true;
23
26
  let cancelled = false;
24
27
  const existing = readVariantChoice(id);
25
- // Create or read a stable session id first
26
28
  const sessionId = getOrCreateSession();
27
29
  void (async () => {
28
30
  try {
@@ -33,14 +35,14 @@ export function Experiment({ id, children }) {
33
35
  return;
34
36
  const serving = resolveVariant(status.serving_variant_id || existing, childrenArray);
35
37
  applyAssignment(id, serving, setActive);
36
- sendImpression(id, serving);
38
+ sendImpression(id, serving, sentImpressionsRef.current);
37
39
  return;
38
40
  }
39
41
  catch {
40
- // fall through to base assignment
42
+ // fall through
41
43
  }
42
44
  }
43
- const baseStatus = await getVariantStatus(id, 'control');
45
+ const baseStatus = await getVariantStatus(id, "control");
44
46
  if (cancelled)
45
47
  return;
46
48
  const weighted = buildWeights(childrenArray, baseStatus.weights);
@@ -57,24 +59,21 @@ export function Experiment({ id, children }) {
57
59
  }
58
60
  const serving = resolveVariant(finalVariant, childrenArray);
59
61
  applyAssignment(id, serving, setActive);
60
- sendImpression(id, serving);
62
+ sendImpression(id, serving, sentImpressionsRef.current);
61
63
  }
62
- catch (err) {
63
- // last-ditch fallback, deterministic equal weight using session id
64
+ catch {
64
65
  const weighted = buildWeights(childrenArray, undefined);
65
66
  const chosen = chooseByWeightDeterministic(weighted, id, sessionId);
66
67
  if (cancelled)
67
68
  return;
68
69
  applyAssignment(id, chosen, setActive);
69
- sendImpression(id, chosen);
70
+ sendImpression(id, chosen, sentImpressionsRef.current);
70
71
  }
71
72
  })();
72
73
  return () => {
73
74
  cancelled = true;
74
75
  };
75
76
  }, [id, childrenArray]);
76
- if (!active)
77
- return null;
78
77
  const match = childrenArray.find((c) => c.id === active);
79
78
  return match ? match.element : null;
80
79
  }
@@ -82,11 +81,12 @@ function applyAssignment(experimentId, variantId, setActive) {
82
81
  persistVariantChoice(experimentId, variantId);
83
82
  setActive(variantId);
84
83
  }
85
- function sendImpression(experimentId, variantId) {
84
+ function sendImpression(experimentId, variantId, sent) {
86
85
  if (!variantId)
87
86
  return;
88
- if (sentImpressions.has(`${experimentId}:${variantId}`))
87
+ const key = `${experimentId}:${variantId}`;
88
+ if (sent.has(key))
89
89
  return;
90
- sentImpressions.add(`${experimentId}:${variantId}`);
91
- void record('impression', { experimentId, variantId });
90
+ sent.add(key);
91
+ void record("impression", { experimentId, variantId });
92
92
  }
@@ -3,7 +3,7 @@ export type VariantProps = {
3
3
  weight?: number;
4
4
  children?: React.ReactNode;
5
5
  };
6
- export declare function Variant({ id, children }: VariantProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function Variant({ children }: VariantProps): import("react/jsx-runtime").JSX.Element;
7
7
  export declare namespace Variant {
8
8
  var displayName: string;
9
9
  }
@@ -1,5 +1,6 @@
1
+ "use client";
1
2
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- export function Variant({ id, children }) {
3
+ export function Variant({ children }) {
3
4
  return _jsx(_Fragment, { children: children });
4
5
  }
5
- Variant.displayName = 'CascaydVariant';
6
+ Variant.displayName = "CascaydVariant";
@@ -0,0 +1,4 @@
1
+ export { Experiment } from './Experiment';
2
+ export { Variant } from './Variant';
3
+ export type { ExperimentProps } from './Experiment';
4
+ export type { VariantProps } from './Variant';
@@ -0,0 +1,2 @@
1
+ export { Experiment } from './Experiment';
2
+ export { Variant } from './Variant';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cascayd/experiment",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "A lightweight A/B testing SDK for React applications with server-side analytics integration",
5
5
  "keywords": [
6
6
  "ab-testing",