@cascayd/experiment 0.3.18 → 0.3.20
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 +1 -1
- package/dist/client.js +13 -5
- package/dist/cookies.js +7 -1
- package/dist/react/Experiment.js +13 -0
- package/dist/shopify-handler-browser.js +28 -13
- package/package.json +1 -1
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
|
|
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,3 +1,4 @@
|
|
|
1
|
+
//! SSR: Imports cookie functions that use document.cookie
|
|
1
2
|
import { getOrCreateSession, readVariantChoice } from './cookies.js';
|
|
2
3
|
let API_KEY = '';
|
|
3
4
|
let BASE_URL = 'https://ab-mvp-backend.onrender.com';
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/react/Experiment.js
CHANGED
|
@@ -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}`;
|
|
@@ -339,10 +339,10 @@
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
// Setup conversion tracking for an experiment
|
|
342
|
-
function setupConversionTracking(experimentId) {
|
|
342
|
+
function setupConversionTracking(experimentId, sdk) {
|
|
343
343
|
const selector = '[data-cascayd-experiment="' + experimentId + '"] [data-cascayd-conversion]';
|
|
344
344
|
const elements = document.querySelectorAll(selector);
|
|
345
|
-
console.log('[cascayd-shopify] 🎯 Setting up conversion tracking for experiment:', experimentId, 'found', elements.length, 'conversion elements');
|
|
345
|
+
console.log('[cascayd-shopify] 🎯 Setting up conversion tracking for experiment:', experimentId, 'found', elements.length, 'conversion elements', 'hasSDK:', !!sdk);
|
|
346
346
|
|
|
347
347
|
elements.forEach(function(el) {
|
|
348
348
|
// Skip if already tracked
|
|
@@ -358,16 +358,31 @@
|
|
|
358
358
|
console.log('[cascayd-shopify] 🎯 Conversion click detected!');
|
|
359
359
|
const variantId = findVariantIdFromElement(el, experimentId);
|
|
360
360
|
if (variantId) {
|
|
361
|
-
console.log('[cascayd-shopify] 📊 Recording conversion:', { experimentId, variantId });
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
361
|
+
console.log('[cascayd-shopify] 📊 Recording conversion:', { experimentId, variantId, hasSDK: !!sdk, hasRecordFn: !!(sdk && sdk.record) });
|
|
362
|
+
|
|
363
|
+
// Use SDK's record function if available (same as impressions), otherwise fallback to direct API call
|
|
364
|
+
if (sdk && sdk.record) {
|
|
365
|
+
console.log('[cascayd-shopify] ✅ Using SDK record function for conversion');
|
|
366
|
+
sdk.record('conversion', {
|
|
367
|
+
experimentId: experimentId,
|
|
368
|
+
variantId: variantId
|
|
369
|
+
}).then(function() {
|
|
370
|
+
console.log('[cascayd-shopify] ✅ Conversion recorded successfully via SDK');
|
|
371
|
+
}).catch(function(error) {
|
|
372
|
+
console.error('[cascayd-shopify] ❌ Failed to record conversion via SDK:', error);
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
console.log('[cascayd-shopify] ⚠️ SDK record not available, using direct API call');
|
|
376
|
+
// Fallback to direct API call
|
|
377
|
+
recordDirect('conversion', {
|
|
378
|
+
experimentId: experimentId,
|
|
379
|
+
variantId: variantId
|
|
380
|
+
}).then(function() {
|
|
381
|
+
console.log('[cascayd-shopify] ✅ Conversion recorded successfully via direct API');
|
|
382
|
+
}).catch(function(error) {
|
|
383
|
+
console.error('[cascayd-shopify] ❌ Failed to record conversion via direct API:', error);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
371
386
|
} else {
|
|
372
387
|
console.warn('[cascayd-shopify] ⚠️ No variant found for experiment', experimentId, 'when recording conversion');
|
|
373
388
|
}
|
|
@@ -450,7 +465,7 @@
|
|
|
450
465
|
});
|
|
451
466
|
|
|
452
467
|
// Setup conversion tracking after variant is shown
|
|
453
|
-
setupConversionTracking(experimentId);
|
|
468
|
+
setupConversionTracking(experimentId, sdk);
|
|
454
469
|
console.log('[cascayd-shopify] ✅ Finished processing experiment:', experimentId);
|
|
455
470
|
}
|
|
456
471
|
|