@cascayd/experiment 0.3.19 → 0.3.21

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/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { InitOptions, ExperimentConfigResponse, EventType, RecordOptions, VariantStatusResponse } from './types';
2
2
  export declare function initCascayd(opts: InitOptions): void;
3
3
  export declare function getVariantStatus(experimentId: string, variantId: string): Promise<VariantStatusResponse>;
4
- export declare function assignVariant(experimentId: string, detectedVariants?: string[], route?: string, shop?: string): Promise<{
4
+ export declare function assignVariant(experimentId: string, detectedVariants?: string[], route?: string): Promise<{
5
5
  variantId: string;
6
6
  config: ExperimentConfigResponse;
7
7
  }>;
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
+ //! SSR: Imports cookie functions that use document.cookie
1
2
  import { getOrCreateSession, readVariantChoice } from './cookies.js';
2
3
  let API_KEY = '';
3
- let BASE_URL = 'https://ab-mvp-backend.onrender.com';
4
+ let BASE_URL = 'https://ab-mvp-backend-bnqr.onrender.com';
4
5
  const SDK_VERSION = '0.2.0-dev';
5
6
  export function initCascayd(opts) {
6
7
  API_KEY = opts.apiKey;
@@ -14,6 +15,7 @@ export function initCascayd(opts) {
14
15
  });
15
16
  }
16
17
  async function fetchConfig(experimentId) {
18
+ //! SSR: fetch works in Node 18+ but may need request context (headers, cookies) for auth
17
19
  const res = await fetch(`${BASE_URL}/experiments/${encodeURIComponent(experimentId)}/config`);
18
20
  if (!res.ok)
19
21
  throw new Error(`Config load failed: ${res.status}`);
@@ -21,6 +23,7 @@ async function fetchConfig(experimentId) {
21
23
  }
22
24
  export async function getVariantStatus(experimentId, variantId) {
23
25
  console.log('[cascayd-sdk] getVariantStatus', experimentId, variantId);
26
+ //! SSR: fetch works in Node 18+ but may need request context (headers, cookies) for auth
24
27
  const res = await fetch(`${BASE_URL}/experiments/${encodeURIComponent(experimentId)}/variants/${encodeURIComponent(variantId)}/status`);
25
28
  const data = (await res.json());
26
29
  console.log('[cascayd-sdk] getVariantStatus response', data);
@@ -44,6 +47,7 @@ function chooseByWeight(variants, experimentId) {
44
47
  console.log('[cascayd-sdk] chooseByWeight called with variants:', variants);
45
48
  const total = variants.reduce((s, v) => s + v.weight, 0);
46
49
  console.log('[cascayd-sdk] chooseByWeight total weight:', total);
50
+ //! SSR: Date.now() is non-deterministic - variant will change on every render/request
47
51
  // Create a seed based on current time (milliseconds) and experiment ID
48
52
  // This makes it change over time while being deterministic for the same millisecond
49
53
  const now = Date.now();
@@ -71,7 +75,7 @@ function chooseByWeight(variants, experimentId) {
71
75
  console.log('[cascayd-sdk] chooseByWeight fallback to:', fallback);
72
76
  return fallback;
73
77
  }
74
- export async function assignVariant(experimentId, detectedVariants, route, shop) {
78
+ export async function assignVariant(experimentId, detectedVariants, route) {
75
79
  const baseStatus = await getVariantStatus(experimentId, 'control');
76
80
  let weights = baseStatus.weights || {};
77
81
  console.log('[cascayd-sdk] assignVariant weights received:', weights);
@@ -86,11 +90,10 @@ export async function assignVariant(experimentId, detectedVariants, route, shop)
86
90
  const ensureBody = {
87
91
  experiment_id: experimentId,
88
92
  variant_ids: detectedVariants,
93
+ //! SSR: window.location.pathname is browser-only, route should be passed explicitly on server
89
94
  route: route || (typeof window !== 'undefined' ? window.location.pathname : "/"),
90
95
  };
91
- if (shop) {
92
- ensureBody.shop = shop;
93
- }
96
+ //! SSR: fetch works in Node 18+ but may need request context (headers, cookies) for auth
94
97
  const ensureRes = await fetch(`${BASE_URL}/experiments/ensure`, {
95
98
  method: 'POST',
96
99
  headers: {
@@ -159,6 +162,7 @@ function baseStatusToVariants(status) {
159
162
  return entries.map(([id, weight]) => ({ id, weight }));
160
163
  }
161
164
  export async function record(type, opts = {}) {
165
+ //! SSR: getOrCreateSession uses document.cookie
162
166
  const sessionId = getOrCreateSession();
163
167
  const body = {
164
168
  type,
@@ -166,6 +170,7 @@ export async function record(type, opts = {}) {
166
170
  };
167
171
  if (opts.experimentId) {
168
172
  body.experiment_id = opts.experimentId;
173
+ //! SSR: readVariantChoice uses document.cookie
169
174
  const fromCookie = readVariantChoice(opts.experimentId);
170
175
  const variantId = opts.variantId || fromCookie;
171
176
  if (variantId) {
@@ -179,10 +184,12 @@ export async function record(type, opts = {}) {
179
184
  body.value = opts.value;
180
185
  if (opts.route) {
181
186
  body.route = opts.route;
187
+ //! SSR: window.location.pathname is browser-only, route should be passed explicitly on server
182
188
  }
183
189
  else if (typeof window !== 'undefined' && window.location) {
184
190
  body.route = window.location.pathname;
185
191
  }
192
+ //! SSR: fetch works in Node 18+ but may need request context (headers, cookies) for auth
186
193
  const res = await fetch(`${BASE_URL}/events`, {
187
194
  method: 'POST',
188
195
  headers: {
@@ -194,7 +201,8 @@ export async function record(type, opts = {}) {
194
201
  if (!res.ok)
195
202
  throw new Error(`Record failed: ${res.status}`);
196
203
  }
197
- // Expose functions to window for browser/Shopify theme compatibility
204
+ //! SSR: Exposing to window object is browser-only, should be conditional or removed for SSR
205
+ // Expose functions to window for browser compatibility
198
206
  if (typeof window !== 'undefined') {
199
207
  ;
200
208
  window.initCascayd = initCascayd;
package/dist/cookies.js CHANGED
@@ -1,19 +1,23 @@
1
1
  function getCookie(name) {
2
+ //! SSR: document.cookie is browser-only, needs server-side cookie abstraction
2
3
  const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()\[\]\\\/\+^]/g, '\\$&') + '=([^;]*)'));
3
4
  return match ? decodeURIComponent(match[1]) : null;
4
5
  }
5
6
  function setCookie(name, value) {
7
+ //! SSR: document.cookie is browser-only, needs server-side cookie abstraction
6
8
  document.cookie = `${name}=${encodeURIComponent(value)}; path=/; SameSite=Lax`;
7
9
  }
8
10
  function randomId() {
9
- // lightweight uuid v4-ish
11
+ // lightweight uuid v4-ish.
10
12
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
13
+ //! SSR: crypto.getRandomValues works in Node 18+ but should use Web Crypto API abstraction
11
14
  const r = (crypto.getRandomValues(new Uint8Array(1))[0] & 0xf) >>> 0;
12
15
  const v = c === 'x' ? r : (r & 0x3) | 0x8;
13
16
  return v.toString(16);
14
17
  });
15
18
  }
16
19
  export function getOrCreateSession() {
20
+ //! SSR: Uses getCookie/setCookie which depend on document.cookie
17
21
  const key = 'cascayd:session';
18
22
  let id = getCookie(key);
19
23
  if (id)
@@ -26,8 +30,10 @@ export function getVariantCookieKey(experimentId) {
26
30
  return `cascayd:${experimentId}`;
27
31
  }
28
32
  export function readVariantChoice(experimentId) {
33
+ //! SSR: Uses getCookie which depends on document.cookie
29
34
  return getCookie(getVariantCookieKey(experimentId));
30
35
  }
31
36
  export function persistVariantChoice(experimentId, variantId) {
37
+ //! SSR: Uses setCookie which depends on document.cookie
32
38
  setCookie(getVariantCookieKey(experimentId), variantId);
33
39
  }
@@ -1,8 +1,11 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
+ //! SSR: Imports client functions that use fetch without request context
2
3
  import { getVariantStatus, record } from '../client';
4
+ //! SSR: Imports cookie functions that use document.cookie
3
5
  import { getOrCreateSession, persistVariantChoice, readVariantChoice } from '../cookies';
4
6
  const sentImpressions = new Set();
5
7
  export function Experiment({ id, children }) {
8
+ //! SSR: Initial null state causes hydration mismatch - server renders null, client renders variant
6
9
  const [active, setActive] = useState(null);
7
10
  const hasRunRef = useRef(false);
8
11
  // collect variant children (with optional weights)
@@ -15,12 +18,15 @@ export function Experiment({ id, children }) {
15
18
  }
16
19
  return arr;
17
20
  }, [children]);
21
+ //! SSR: useEffect doesn't run during SSR, variant assignment happens only on client
18
22
  useEffect(() => {
19
23
  if (hasRunRef.current)
20
24
  return;
21
25
  hasRunRef.current = true;
26
+ //! SSR: readVariantChoice uses document.cookie
22
27
  const existing = readVariantChoice(id);
23
28
  let cancelled = false;
29
+ //! SSR: getOrCreateSession uses document.cookie
24
30
  getOrCreateSession();
25
31
  void (async () => {
26
32
  try {
@@ -31,6 +37,7 @@ export function Experiment({ id, children }) {
31
37
  });
32
38
  console.log('[cascayd-sdk] current children', childrenArray.map((c) => c.id));
33
39
  try {
40
+ //! SSR: getVariantStatus uses fetch without request context
34
41
  const status = await getVariantStatus(id, existing);
35
42
  if (cancelled)
36
43
  return;
@@ -51,6 +58,7 @@ export function Experiment({ id, children }) {
51
58
  }
52
59
  }
53
60
  console.log('[cascayd-sdk] fetching base status', { experimentId: id, variantId: 'control' });
61
+ //! SSR: getVariantStatus uses fetch without request context
54
62
  const baseStatus = await getVariantStatus(id, 'control');
55
63
  if (cancelled)
56
64
  return;
@@ -67,6 +75,7 @@ export function Experiment({ id, children }) {
67
75
  variantId: candidate,
68
76
  });
69
77
  try {
78
+ //! SSR: getVariantStatus uses fetch without request context
70
79
  const status = await getVariantStatus(id, candidate);
71
80
  if (cancelled)
72
81
  return;
@@ -102,6 +111,7 @@ export function Experiment({ id, children }) {
102
111
  cancelled = true;
103
112
  };
104
113
  }, [id, childrenArray]);
114
+ //! SSR: Returning null on server causes hydration mismatch - server renders null, client renders variant
105
115
  if (!active)
106
116
  return null;
107
117
  const match = childrenArray.find((c) => c.id === active);
@@ -135,6 +145,7 @@ function resolveVariant(candidate, children) {
135
145
  return children[0]?.id ?? 'control';
136
146
  }
137
147
  function applyAssignment(experimentId, variantId, setActive) {
148
+ //! SSR: persistVariantChoice uses document.cookie
138
149
  persistVariantChoice(experimentId, variantId);
139
150
  setActive(variantId);
140
151
  }
@@ -144,6 +155,7 @@ function sendImpression(experimentId, variantId) {
144
155
  if (sentImpressions.has(`${experimentId}:${variantId}`))
145
156
  return;
146
157
  sentImpressions.add(`${experimentId}:${variantId}`);
158
+ //! SSR: record uses fetch and cookies without request context
147
159
  void record('impression', { experimentId, variantId });
148
160
  }
149
161
  function fallbackAssign(children, experimentId) {
@@ -165,6 +177,7 @@ function seededRandom(seed) {
165
177
  function chooseByWeight(items, experimentId) {
166
178
  console.log('[cascayd-sdk] choosing by weight', items);
167
179
  const total = items.reduce((sum, item) => sum + item.weight, 0) || 1;
180
+ //! SSR: Date.now() is non-deterministic - variant will change on every render/request
168
181
  // Create a seed based on current time (milliseconds) and experiment ID
169
182
  const now = Date.now();
170
183
  const seedString = `${experimentId}-${now}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cascayd/experiment",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "description": "A lightweight A/B testing SDK for React applications with server-side analytics integration",
5
5
  "keywords": [
6
6
  "ab-testing",