@cascayd/experiment 0.3.11 → 0.3.14

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.js CHANGED
@@ -168,11 +168,21 @@ export async function record(type, opts = {}) {
168
168
  body.experiment_id = opts.experimentId;
169
169
  const fromCookie = readVariantChoice(opts.experimentId);
170
170
  const variantId = opts.variantId || fromCookie;
171
- if (variantId)
171
+ if (variantId) {
172
172
  body.variant_id = variantId;
173
+ }
174
+ else if (opts.experimentId) {
175
+ console.warn(`[cascayd-sdk] No variant_id found for experiment ${opts.experimentId}`);
176
+ }
173
177
  }
174
178
  if (typeof opts.value === 'number')
175
179
  body.value = opts.value;
180
+ if (opts.route) {
181
+ body.route = opts.route;
182
+ }
183
+ else if (typeof window !== 'undefined' && window.location) {
184
+ body.route = window.location.pathname;
185
+ }
176
186
  const res = await fetch(`${BASE_URL}/events`, {
177
187
  method: 'POST',
178
188
  headers: {
@@ -1,433 +1,433 @@
1
- /**
2
- * Cascayd A/B Testing - Shopify Theme Handler (Browser Standalone)
3
- * This script automatically handles variant selection for elements with
4
- * data-cascayd-experiment and data-cascayd-variant attributes
5
- *
6
- * Usage:
7
- * 1. Load the Cascayd SDK first:
8
- * <script type="module" src="https://cdn.jsdelivr.net/npm/@cascayd/experiment@latest/dist/client.js"></script>
9
- * 2. Load this handler:
10
- * <script src="https://cdn.jsdelivr.net/npm/@cascayd/experiment@latest/dist/shopify-handler-browser.js"></script>
11
- *
12
- * Or if SDK is loaded as a module that exposes functions to window:
13
- * <script>
14
- * window.initCascayd({ apiKey: 'YOUR_KEY' });
15
- * </script>
16
- */
17
-
18
- (function() {
19
- 'use strict';
20
-
21
- console.log('[cascayd-shopify] 🚀 Handler script starting...');
22
- console.log('[cascayd-shopify] Document readyState:', document.readyState);
23
- console.log('[cascayd-shopify] Window object available:', typeof window !== 'undefined');
24
-
25
- // Cookie helper functions (matching SDK's cookie logic)
26
- function getCookie(name) {
27
- const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()\[\]\\\/\+^]/g, '\\$&') + '=([^;]*)'));
28
- const value = match ? decodeURIComponent(match[1]) : null;
29
- console.log('[cascayd-shopify] đŸ“Ļ getCookie:', name, '=', value);
30
- return value;
31
- }
32
-
33
- function setCookie(name, value) {
34
- document.cookie = `${name}=${encodeURIComponent(value)}; path=/; SameSite=Lax; max-age=31536000`;
35
- }
36
-
37
- function getVariantCookieKey(experimentId) {
38
- return `cascayd:${experimentId}`;
39
- }
40
-
41
- function readVariantChoice(experimentId) {
42
- const choice = getCookie(getVariantCookieKey(experimentId));
43
- console.log('[cascayd-shopify] 📖 Read variant choice:', { experimentId, choice });
44
- return choice;
45
- }
46
-
47
- function persistVariantChoice(experimentId, variantId) {
48
- const key = getVariantCookieKey(experimentId);
49
- console.log('[cascayd-shopify] 💾 Persisting variant choice:', { experimentId, variantId, cookieKey: key });
50
- setCookie(key, variantId);
51
- }
52
-
53
- // API configuration (same as SDK)
54
- const BASE_URL = 'https://ab-mvp-backend.onrender.com';
55
- let API_KEY = '';
56
-
57
- // Try to get API key from window (if SDK was initialized)
58
- function getAPIKey() {
59
- if (typeof window !== 'undefined' && window._cascaydAPIKey) {
60
- return window._cascaydAPIKey;
61
- }
62
- return API_KEY;
63
- }
64
-
65
- // Direct API call functions (same logic as SDK)
66
- async function getVariantStatus(experimentId, variantId) {
67
- const url = BASE_URL + '/experiments/' + encodeURIComponent(experimentId) + '/variants/' + encodeURIComponent(variantId) + '/status';
68
- console.log('[cascayd-shopify] 🌐 Fetching variant status:', { experimentId, variantId, url });
69
- try {
70
- const res = await fetch(url);
71
- const data = await res.json();
72
- console.log('[cascayd-shopify] ✅ Variant status response:', { status: res.status, data });
73
- if (!res.ok) {
74
- const err = new Error('Variant status failed: ' + res.status);
75
- err.payload = data;
76
- throw err;
77
- }
78
- return data;
79
- } catch (error) {
80
- console.error('[cascayd-shopify] ❌ getVariantStatus error:', error);
81
- throw error;
82
- }
83
- }
84
-
85
- function chooseByWeight(variants) {
86
- const total = variants.reduce(function(s, v) { return s + v.weight; }, 0);
87
- const r = Math.random() * total;
88
- let acc = 0;
89
- for (let i = 0; i < variants.length; i++) {
90
- acc += variants[i].weight;
91
- if (r <= acc) {
92
- return variants[i].id;
93
- }
94
- }
95
- return variants[variants.length - 1] ? variants[variants.length - 1].id : 'control';
96
- }
97
-
98
- function baseStatusToVariants(status) {
99
- const entries = Object.entries(status.weights || {});
100
- if (entries.length === 0) {
101
- return [{ id: 'control', weight: 1 }];
102
- }
103
- return entries.map(function(entry) {
104
- return { id: entry[0], weight: entry[1] };
105
- });
106
- }
107
-
108
- // Assign variant using same logic as SDK
109
- async function assignVariantDirect(experimentId) {
110
- console.log('[cascayd-shopify] 🎲 assignVariantDirect called for:', experimentId);
111
- const baseStatus = await getVariantStatus(experimentId, 'control');
112
- console.log('[cascayd-shopify] 📊 Base status received:', baseStatus);
113
- const weights = baseStatus.weights || {};
114
- const variants = Object.entries(weights).map(function(entry) {
115
- return { id: entry[0], weight: entry[1] };
116
- });
117
- console.log('[cascayd-shopify] 📋 Variants from weights:', variants);
118
-
119
- const allVariants = variants.length > 0 ? variants : baseStatusToVariants(baseStatus);
120
- console.log('[cascayd-shopify] đŸŽ¯ All variants for selection:', allVariants);
121
- let candidate = chooseByWeight(allVariants);
122
- console.log('[cascayd-shopify] 🎲 Selected candidate:', candidate);
123
- let finalStatus = null;
124
-
125
- if (candidate) {
126
- try {
127
- finalStatus = await getVariantStatus(experimentId, candidate);
128
- console.log('[cascayd-shopify] ✅ Final status for candidate:', finalStatus);
129
- } catch (error) {
130
- console.error('[cascayd-shopify] ❌ getVariantStatus failed for candidate', { experimentId: experimentId, candidate: candidate, error: error });
131
- // Fall back to candidate
132
- }
133
- }
134
-
135
- const serving = (finalStatus && finalStatus.serving_variant_id) || candidate || 'control';
136
- console.log('[cascayd-shopify] ✅ Final serving variant:', serving);
137
- return { variantId: serving };
138
- }
139
-
140
- // Record event using same logic as SDK
141
- async function recordDirect(type, opts) {
142
- const sessionId = getOrCreateSession();
143
- const body = {
144
- type: type,
145
- session_id: sessionId
146
- };
147
-
148
- if (opts.experimentId) {
149
- body.experiment_id = opts.experimentId;
150
- const fromCookie = readVariantChoice(opts.experimentId);
151
- const variantId = opts.variantId || fromCookie;
152
- if (variantId) {
153
- body.variant_id = variantId;
154
- }
155
- }
156
-
157
- if (typeof opts.value === 'number') {
158
- body.value = opts.value;
159
- }
160
-
161
- const headers = {
162
- 'Content-Type': 'application/json'
163
- };
164
- const apiKey = getAPIKey();
165
- if (apiKey) {
166
- headers.Authorization = 'Bearer ' + apiKey;
167
- }
168
-
169
- const res = await fetch(BASE_URL + '/events', {
170
- method: 'POST',
171
- headers: headers,
172
- body: JSON.stringify(body)
173
- });
174
-
175
- if (!res.ok) {
176
- throw new Error('Record failed: ' + res.status);
177
- }
178
- }
179
-
180
- function getOrCreateSession() {
181
- const key = 'cascayd:session';
182
- let id = getCookie(key);
183
- if (id) return id;
184
- id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
185
- const r = (crypto.getRandomValues(new Uint8Array(1))[0] & 0xf) >>> 0;
186
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
187
- return v.toString(16);
188
- });
189
- setCookie(key, id);
190
- return id;
191
- }
192
-
193
- // Try to use SDK functions if available, otherwise use direct API calls
194
- async function waitForSDK(maxAttempts, interval) {
195
- maxAttempts = maxAttempts || 10;
196
- interval = interval || 100;
197
- console.log('[cascayd-shopify] âŗ Waiting for SDK...', { maxAttempts, interval });
198
-
199
- return new Promise(function(resolve) {
200
- let attempts = 0;
201
- const checkSDK = function() {
202
- attempts++;
203
- console.log('[cascayd-shopify] 🔍 Checking for SDK (attempt ' + attempts + '/' + maxAttempts + ')');
204
-
205
- // Check if SDK functions are available on window
206
- let assignVariantFn = null;
207
- let recordFn = null;
208
-
209
- if (typeof window !== 'undefined') {
210
- console.log('[cascayd-shopify] đŸĒŸ Window available, checking functions...');
211
- console.log('[cascayd-shopify] - window.assignVariant:', typeof window.assignVariant);
212
- console.log('[cascayd-shopify] - window.record:', typeof window.record);
213
- console.log('[cascayd-shopify] - window.Cascayd:', typeof window.Cascayd);
214
- console.log('[cascayd-shopify] - window.initCascayd:', typeof window.initCascayd);
215
-
216
- if (window.assignVariant) {
217
- assignVariantFn = window.assignVariant;
218
- console.log('[cascayd-shopify] ✅ Found window.assignVariant');
219
- }
220
- if (window.record) {
221
- recordFn = window.record;
222
- console.log('[cascayd-shopify] ✅ Found window.record');
223
- }
224
- // Try Cascayd namespace
225
- if (!assignVariantFn && window.Cascayd && window.Cascayd.assignVariant) {
226
- assignVariantFn = window.Cascayd.assignVariant;
227
- console.log('[cascayd-shopify] ✅ Found Cascayd.assignVariant');
228
- }
229
- if (!recordFn && window.Cascayd && window.Cascayd.record) {
230
- recordFn = window.Cascayd.record;
231
- console.log('[cascayd-shopify] ✅ Found Cascayd.record');
232
- }
233
- }
234
-
235
- if (assignVariantFn && recordFn) {
236
- console.log('[cascayd-shopify] ✅ SDK functions found! Using SDK.');
237
- resolve({ assignVariant: assignVariantFn, record: recordFn });
238
- } else if (attempts >= maxAttempts) {
239
- console.log('[cascayd-shopify] âš ī¸ SDK not found after ' + maxAttempts + ' attempts. Using direct API calls.');
240
- // Use direct API calls as fallback
241
- resolve({ assignVariant: assignVariantDirect, record: recordDirect });
242
- } else {
243
- console.log('[cascayd-shopify] âŗ SDK not ready, retrying in ' + interval + 'ms...');
244
- setTimeout(checkSDK, interval);
245
- }
246
- };
247
- checkSDK();
248
- });
249
- }
250
-
251
- // Record impression event
252
- async function recordImpression(experimentId, variantId, sdk) {
253
- console.log('[cascayd-shopify] 📝 Recording impression:', { experimentId, variantId, hasSDK: !!sdk, hasRecordFn: !!(sdk && sdk.record) });
254
- if (!sdk || !sdk.record) {
255
- console.warn('[cascayd-shopify] âš ī¸ Record function not available, skipping impression tracking');
256
- return;
257
- }
258
-
259
- try {
260
- await sdk.record('impression', {
261
- experimentId: experimentId,
262
- variantId: variantId
263
- });
264
- console.log('[cascayd-shopify] ✅ Impression recorded successfully');
265
- } catch (error) {
266
- console.error('[cascayd-shopify] ❌ Failed to record impression:', error);
267
- }
268
- }
269
-
270
- // Process a single experiment
271
- async function processExperiment(experimentId, variantElements, sdk) {
272
- console.log('[cascayd-shopify] đŸ”Ŧ Processing experiment:', { experimentId, variantElementCount: variantElements.length, hasSDK: !!sdk });
273
-
274
- // Check if we already have a variant choice stored
275
- let variantId = readVariantChoice(experimentId);
276
- console.log('[cascayd-shopify] 📖 Variant choice from cookie:', variantId);
277
-
278
- // If no stored choice, assign a variant
279
- if (!variantId) {
280
- console.log('[cascayd-shopify] 🆕 No stored variant, assigning new one...');
281
- if (sdk && sdk.assignVariant) {
282
- try {
283
- console.log('[cascayd-shopify] 🎲 Calling SDK assignVariant...');
284
- const result = await sdk.assignVariant(experimentId);
285
- variantId = result.variantId;
286
- console.log('[cascayd-shopify] ✅ SDK assigned variant:', variantId);
287
- persistVariantChoice(experimentId, variantId);
288
- } catch (error) {
289
- console.error('[cascayd-shopify] ❌ Failed to assign variant via SDK:', error);
290
- // Fallback: use first available variant or control
291
- const variants = [];
292
- variantElements.forEach(function(el) {
293
- const vId = el.getAttribute('data-cascayd-variant');
294
- if (vId && variants.indexOf(vId) === -1) {
295
- variants.push(vId);
296
- }
297
- });
298
- variantId = variants.indexOf('control') >= 0 ? 'control' : (variants[0] || 'control');
299
- console.log('[cascayd-shopify] 🔄 Using fallback variant:', variantId, 'from available:', variants);
300
- persistVariantChoice(experimentId, variantId);
301
- }
302
- } else {
303
- console.error('[cascayd-shopify] ❌ No assignVariant function available');
304
- // This shouldn't happen since waitForSDK always returns functions (direct API or SDK)
305
- }
306
- } else {
307
- console.log('[cascayd-shopify] ✅ Using existing variant choice:', variantId);
308
- }
309
-
310
- // Show the selected variant, hide all others
311
- console.log('[cascayd-shopify] đŸ‘ī¸ Showing/hiding variants. Selected:', variantId);
312
- let hasRecorded = false;
313
- variantElements.forEach(function(el, index) {
314
- const elVariantId = el.getAttribute('data-cascayd-variant');
315
- console.log('[cascayd-shopify] Element ' + (index + 1) + ':', { variantId: elVariantId, matches: elVariantId === variantId });
316
-
317
- if (elVariantId === variantId) {
318
- console.log('[cascayd-shopify] ✅ Showing element with variant:', elVariantId);
319
- // Remove CSS hiding and mark as visible
320
- el.classList.remove('cascayd-hidden');
321
- el.classList.add('cascayd-visible');
322
- // Detect if element should be inline or block based on original display
323
- const computedStyle = window.getComputedStyle(el);
324
- if (computedStyle.display === 'inline' || computedStyle.display === 'inline-block') {
325
- el.classList.add('cascayd-visible-inline');
326
- }
327
- el.style.display = '';
328
- el.removeAttribute('hidden');
329
- console.log('[cascayd-shopify] ✅ Element shown, classes:', el.className);
330
- // Record impression only once for the shown variant
331
- if (!hasRecorded) {
332
- recordImpression(experimentId, variantId, sdk);
333
- hasRecorded = true;
334
- }
335
- } else {
336
- console.log('[cascayd-shopify] đŸšĢ Hiding element with variant:', elVariantId);
337
- el.style.display = 'none';
338
- el.setAttribute('hidden', 'hidden');
339
- el.classList.add('cascayd-hidden');
340
- el.classList.remove('cascayd-visible', 'cascayd-visible-inline');
341
- }
342
- });
343
- console.log('[cascayd-shopify] ✅ Finished processing experiment:', experimentId);
344
- }
345
-
346
- // Main initialization function
347
- async function initCascaydExperiments() {
348
- console.log('[cascayd-shopify] đŸŽ¯ initCascaydExperiments called');
349
- console.log('[cascayd-shopify] 📄 Document readyState:', document.readyState);
350
-
351
- // Wait for SDK to be loaded (with timeout)
352
- console.log('[cascayd-shopify] âŗ Waiting for SDK...');
353
- const sdk = await waitForSDK(50, 100);
354
- console.log('[cascayd-shopify] ✅ SDK ready:', { hasSDK: !!sdk, hasAssignVariant: !!(sdk && sdk.assignVariant), hasRecord: !!(sdk && sdk.record) });
355
-
356
- // Find all elements with data-cascayd-experiment attribute
357
- const allExperimentElements = document.querySelectorAll('[data-cascayd-experiment]');
358
- console.log('[cascayd-shopify] 🔍 Found experiment elements:', allExperimentElements.length);
359
-
360
- if (allExperimentElements.length === 0) {
361
- console.log('[cascayd-shopify] â„šī¸ No experiment elements found, exiting');
362
- return; // No experiments to process
363
- }
364
-
365
- // Log all found elements
366
- allExperimentElements.forEach(function(el, index) {
367
- const expId = el.getAttribute('data-cascayd-experiment');
368
- const varId = el.getAttribute('data-cascayd-variant');
369
- console.log('[cascayd-shopify] Element ' + (index + 1) + ':', { experimentId: expId, variantId: varId, tagName: el.tagName });
370
- });
371
-
372
- // Group elements by experiment ID
373
- const experiments = new Map();
374
-
375
- allExperimentElements.forEach(function(el) {
376
- const experimentId = el.getAttribute('data-cascayd-experiment');
377
- if (!experimentId) {
378
- console.warn('[cascayd-shopify] âš ī¸ Element missing experiment ID:', el);
379
- return;
380
- }
381
-
382
- if (!experiments.has(experimentId)) {
383
- experiments.set(experimentId, []);
384
- }
385
- experiments.get(experimentId).push(el);
386
- });
387
-
388
- console.log('[cascayd-shopify] 📊 Grouped experiments:', Array.from(experiments.keys()).map(function(id) {
389
- return { experimentId: id, variantCount: experiments.get(id).length };
390
- }));
391
-
392
- // Process each experiment
393
- const promises = [];
394
- experiments.forEach(function(variantElements, experimentId) {
395
- console.log('[cascayd-shopify] 🚀 Processing experiment:', experimentId, 'with', variantElements.length, 'variants');
396
- promises.push(processExperiment(experimentId, variantElements, sdk));
397
- });
398
-
399
- console.log('[cascayd-shopify] âŗ Waiting for all experiments to process...');
400
- await Promise.all(promises);
401
- console.log('[cascayd-shopify] ✅ All experiments processed!');
402
- }
403
-
404
- // Initialize when DOM is ready
405
- console.log('[cascayd-shopify] 🔄 Setting up initialization...');
406
- if (document.readyState === 'loading') {
407
- console.log('[cascayd-shopify] âŗ DOM still loading, waiting for DOMContentLoaded...');
408
- document.addEventListener('DOMContentLoaded', function() {
409
- console.log('[cascayd-shopify] ✅ DOMContentLoaded fired!');
410
- initCascaydExperiments();
411
- });
412
- } else {
413
- // DOM already ready
414
- console.log('[cascayd-shopify] ✅ DOM already ready, initializing immediately');
415
- initCascaydExperiments();
416
- }
417
-
418
- // Handle Shopify's dynamic content (sections loaded via AJAX)
419
- if (typeof window.addEventListener !== 'undefined') {
420
- console.log('[cascayd-shopify] 🛒 Setting up Shopify section event listeners...');
421
- document.addEventListener('shopify:section:load', function() {
422
- console.log('[cascayd-shopify] 🔄 shopify:section:load event fired, re-initializing...');
423
- initCascaydExperiments();
424
- });
425
- document.addEventListener('shopify:section:reorder', function() {
426
- console.log('[cascayd-shopify] 🔄 shopify:section:reorder event fired, re-initializing...');
427
- initCascaydExperiments();
428
- });
429
- }
430
-
431
- console.log('[cascayd-shopify] ✅ Handler script initialization complete');
432
- })();
433
-
1
+ /**
2
+ * Cascayd A/B Testing - Shopify Theme Handler (Browser Standalone)
3
+ * This script automatically handles variant selection for elements with
4
+ * data-cascayd-experiment and data-cascayd-variant attributes
5
+ *
6
+ * Usage:
7
+ * 1. Load the Cascayd SDK first:
8
+ * <script type="module" src="https://cdn.jsdelivr.net/npm/@cascayd/experiment@latest/dist/client.js"></script>
9
+ * 2. Load this handler:
10
+ * <script src="https://cdn.jsdelivr.net/npm/@cascayd/experiment@latest/dist/shopify-handler-browser.js"></script>
11
+ *
12
+ * Or if SDK is loaded as a module that exposes functions to window:
13
+ * <script>
14
+ * window.initCascayd({ apiKey: 'YOUR_KEY' });
15
+ * </script>
16
+ */
17
+
18
+ (function() {
19
+ 'use strict';
20
+
21
+ console.log('[cascayd-shopify] 🚀 Handler script starting...');
22
+ console.log('[cascayd-shopify] Document readyState:', document.readyState);
23
+ console.log('[cascayd-shopify] Window object available:', typeof window !== 'undefined');
24
+
25
+ // Cookie helper functions (matching SDK's cookie logic)
26
+ function getCookie(name) {
27
+ const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()\[\]\\\/\+^]/g, '\\$&') + '=([^;]*)'));
28
+ const value = match ? decodeURIComponent(match[1]) : null;
29
+ console.log('[cascayd-shopify] đŸ“Ļ getCookie:', name, '=', value);
30
+ return value;
31
+ }
32
+
33
+ function setCookie(name, value) {
34
+ document.cookie = `${name}=${encodeURIComponent(value)}; path=/; SameSite=Lax; max-age=31536000`;
35
+ }
36
+
37
+ function getVariantCookieKey(experimentId) {
38
+ return `cascayd:${experimentId}`;
39
+ }
40
+
41
+ function readVariantChoice(experimentId) {
42
+ const choice = getCookie(getVariantCookieKey(experimentId));
43
+ console.log('[cascayd-shopify] 📖 Read variant choice:', { experimentId, choice });
44
+ return choice;
45
+ }
46
+
47
+ function persistVariantChoice(experimentId, variantId) {
48
+ const key = getVariantCookieKey(experimentId);
49
+ console.log('[cascayd-shopify] 💾 Persisting variant choice:', { experimentId, variantId, cookieKey: key });
50
+ setCookie(key, variantId);
51
+ }
52
+
53
+ // API configuration (same as SDK)
54
+ const BASE_URL = 'https://ab-mvp-backend.onrender.com';
55
+ let API_KEY = '';
56
+
57
+ // Try to get API key from window (if SDK was initialized)
58
+ function getAPIKey() {
59
+ if (typeof window !== 'undefined' && window._cascaydAPIKey) {
60
+ return window._cascaydAPIKey;
61
+ }
62
+ return API_KEY;
63
+ }
64
+
65
+ // Direct API call functions (same logic as SDK)
66
+ async function getVariantStatus(experimentId, variantId) {
67
+ const url = BASE_URL + '/experiments/' + encodeURIComponent(experimentId) + '/variants/' + encodeURIComponent(variantId) + '/status';
68
+ console.log('[cascayd-shopify] 🌐 Fetching variant status:', { experimentId, variantId, url });
69
+ try {
70
+ const res = await fetch(url);
71
+ const data = await res.json();
72
+ console.log('[cascayd-shopify] ✅ Variant status response:', { status: res.status, data });
73
+ if (!res.ok) {
74
+ const err = new Error('Variant status failed: ' + res.status);
75
+ err.payload = data;
76
+ throw err;
77
+ }
78
+ return data;
79
+ } catch (error) {
80
+ console.error('[cascayd-shopify] ❌ getVariantStatus error:', error);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ function chooseByWeight(variants) {
86
+ const total = variants.reduce(function(s, v) { return s + v.weight; }, 0);
87
+ const r = Math.random() * total;
88
+ let acc = 0;
89
+ for (let i = 0; i < variants.length; i++) {
90
+ acc += variants[i].weight;
91
+ if (r <= acc) {
92
+ return variants[i].id;
93
+ }
94
+ }
95
+ return variants[variants.length - 1] ? variants[variants.length - 1].id : 'control';
96
+ }
97
+
98
+ function baseStatusToVariants(status) {
99
+ const entries = Object.entries(status.weights || {});
100
+ if (entries.length === 0) {
101
+ return [{ id: 'control', weight: 1 }];
102
+ }
103
+ return entries.map(function(entry) {
104
+ return { id: entry[0], weight: entry[1] };
105
+ });
106
+ }
107
+
108
+ // Assign variant using same logic as SDK
109
+ async function assignVariantDirect(experimentId) {
110
+ console.log('[cascayd-shopify] 🎲 assignVariantDirect called for:', experimentId);
111
+ const baseStatus = await getVariantStatus(experimentId, 'control');
112
+ console.log('[cascayd-shopify] 📊 Base status received:', baseStatus);
113
+ const weights = baseStatus.weights || {};
114
+ const variants = Object.entries(weights).map(function(entry) {
115
+ return { id: entry[0], weight: entry[1] };
116
+ });
117
+ console.log('[cascayd-shopify] 📋 Variants from weights:', variants);
118
+
119
+ const allVariants = variants.length > 0 ? variants : baseStatusToVariants(baseStatus);
120
+ console.log('[cascayd-shopify] đŸŽ¯ All variants for selection:', allVariants);
121
+ let candidate = chooseByWeight(allVariants);
122
+ console.log('[cascayd-shopify] 🎲 Selected candidate:', candidate);
123
+ let finalStatus = null;
124
+
125
+ if (candidate) {
126
+ try {
127
+ finalStatus = await getVariantStatus(experimentId, candidate);
128
+ console.log('[cascayd-shopify] ✅ Final status for candidate:', finalStatus);
129
+ } catch (error) {
130
+ console.error('[cascayd-shopify] ❌ getVariantStatus failed for candidate', { experimentId: experimentId, candidate: candidate, error: error });
131
+ // Fall back to candidate
132
+ }
133
+ }
134
+
135
+ const serving = (finalStatus && finalStatus.serving_variant_id) || candidate || 'control';
136
+ console.log('[cascayd-shopify] ✅ Final serving variant:', serving);
137
+ return { variantId: serving };
138
+ }
139
+
140
+ // Record event using same logic as SDK
141
+ async function recordDirect(type, opts) {
142
+ const sessionId = getOrCreateSession();
143
+ const body = {
144
+ type: type,
145
+ session_id: sessionId
146
+ };
147
+
148
+ if (opts.experimentId) {
149
+ body.experiment_id = opts.experimentId;
150
+ const fromCookie = readVariantChoice(opts.experimentId);
151
+ const variantId = opts.variantId || fromCookie;
152
+ if (variantId) {
153
+ body.variant_id = variantId;
154
+ }
155
+ }
156
+
157
+ if (typeof opts.value === 'number') {
158
+ body.value = opts.value;
159
+ }
160
+
161
+ const headers = {
162
+ 'Content-Type': 'application/json'
163
+ };
164
+ const apiKey = getAPIKey();
165
+ if (apiKey) {
166
+ headers.Authorization = 'Bearer ' + apiKey;
167
+ }
168
+
169
+ const res = await fetch(BASE_URL + '/events', {
170
+ method: 'POST',
171
+ headers: headers,
172
+ body: JSON.stringify(body)
173
+ });
174
+
175
+ if (!res.ok) {
176
+ throw new Error('Record failed: ' + res.status);
177
+ }
178
+ }
179
+
180
+ function getOrCreateSession() {
181
+ const key = 'cascayd:session';
182
+ let id = getCookie(key);
183
+ if (id) return id;
184
+ id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
185
+ const r = (crypto.getRandomValues(new Uint8Array(1))[0] & 0xf) >>> 0;
186
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
187
+ return v.toString(16);
188
+ });
189
+ setCookie(key, id);
190
+ return id;
191
+ }
192
+
193
+ // Try to use SDK functions if available, otherwise use direct API calls
194
+ async function waitForSDK(maxAttempts, interval) {
195
+ maxAttempts = maxAttempts || 10;
196
+ interval = interval || 100;
197
+ console.log('[cascayd-shopify] âŗ Waiting for SDK...', { maxAttempts, interval });
198
+
199
+ return new Promise(function(resolve) {
200
+ let attempts = 0;
201
+ const checkSDK = function() {
202
+ attempts++;
203
+ console.log('[cascayd-shopify] 🔍 Checking for SDK (attempt ' + attempts + '/' + maxAttempts + ')');
204
+
205
+ // Check if SDK functions are available on window
206
+ let assignVariantFn = null;
207
+ let recordFn = null;
208
+
209
+ if (typeof window !== 'undefined') {
210
+ console.log('[cascayd-shopify] đŸĒŸ Window available, checking functions...');
211
+ console.log('[cascayd-shopify] - window.assignVariant:', typeof window.assignVariant);
212
+ console.log('[cascayd-shopify] - window.record:', typeof window.record);
213
+ console.log('[cascayd-shopify] - window.Cascayd:', typeof window.Cascayd);
214
+ console.log('[cascayd-shopify] - window.initCascayd:', typeof window.initCascayd);
215
+
216
+ if (window.assignVariant) {
217
+ assignVariantFn = window.assignVariant;
218
+ console.log('[cascayd-shopify] ✅ Found window.assignVariant');
219
+ }
220
+ if (window.record) {
221
+ recordFn = window.record;
222
+ console.log('[cascayd-shopify] ✅ Found window.record');
223
+ }
224
+ // Try Cascayd namespace
225
+ if (!assignVariantFn && window.Cascayd && window.Cascayd.assignVariant) {
226
+ assignVariantFn = window.Cascayd.assignVariant;
227
+ console.log('[cascayd-shopify] ✅ Found Cascayd.assignVariant');
228
+ }
229
+ if (!recordFn && window.Cascayd && window.Cascayd.record) {
230
+ recordFn = window.Cascayd.record;
231
+ console.log('[cascayd-shopify] ✅ Found Cascayd.record');
232
+ }
233
+ }
234
+
235
+ if (assignVariantFn && recordFn) {
236
+ console.log('[cascayd-shopify] ✅ SDK functions found! Using SDK.');
237
+ resolve({ assignVariant: assignVariantFn, record: recordFn });
238
+ } else if (attempts >= maxAttempts) {
239
+ console.log('[cascayd-shopify] âš ī¸ SDK not found after ' + maxAttempts + ' attempts. Using direct API calls.');
240
+ // Use direct API calls as fallback
241
+ resolve({ assignVariant: assignVariantDirect, record: recordDirect });
242
+ } else {
243
+ console.log('[cascayd-shopify] âŗ SDK not ready, retrying in ' + interval + 'ms...');
244
+ setTimeout(checkSDK, interval);
245
+ }
246
+ };
247
+ checkSDK();
248
+ });
249
+ }
250
+
251
+ // Record impression event
252
+ async function recordImpression(experimentId, variantId, sdk) {
253
+ console.log('[cascayd-shopify] 📝 Recording impression:', { experimentId, variantId, hasSDK: !!sdk, hasRecordFn: !!(sdk && sdk.record) });
254
+ if (!sdk || !sdk.record) {
255
+ console.warn('[cascayd-shopify] âš ī¸ Record function not available, skipping impression tracking');
256
+ return;
257
+ }
258
+
259
+ try {
260
+ await sdk.record('impression', {
261
+ experimentId: experimentId,
262
+ variantId: variantId
263
+ });
264
+ console.log('[cascayd-shopify] ✅ Impression recorded successfully');
265
+ } catch (error) {
266
+ console.error('[cascayd-shopify] ❌ Failed to record impression:', error);
267
+ }
268
+ }
269
+
270
+ // Process a single experiment
271
+ async function processExperiment(experimentId, variantElements, sdk) {
272
+ console.log('[cascayd-shopify] đŸ”Ŧ Processing experiment:', { experimentId, variantElementCount: variantElements.length, hasSDK: !!sdk });
273
+
274
+ // Check if we already have a variant choice stored
275
+ let variantId = readVariantChoice(experimentId);
276
+ console.log('[cascayd-shopify] 📖 Variant choice from cookie:', variantId);
277
+
278
+ // If no stored choice, assign a variant
279
+ if (!variantId) {
280
+ console.log('[cascayd-shopify] 🆕 No stored variant, assigning new one...');
281
+ if (sdk && sdk.assignVariant) {
282
+ try {
283
+ console.log('[cascayd-shopify] 🎲 Calling SDK assignVariant...');
284
+ const result = await sdk.assignVariant(experimentId);
285
+ variantId = result.variantId;
286
+ console.log('[cascayd-shopify] ✅ SDK assigned variant:', variantId);
287
+ persistVariantChoice(experimentId, variantId);
288
+ } catch (error) {
289
+ console.error('[cascayd-shopify] ❌ Failed to assign variant via SDK:', error);
290
+ // Fallback: use first available variant or control
291
+ const variants = [];
292
+ variantElements.forEach(function(el) {
293
+ const vId = el.getAttribute('data-cascayd-variant');
294
+ if (vId && variants.indexOf(vId) === -1) {
295
+ variants.push(vId);
296
+ }
297
+ });
298
+ variantId = variants.indexOf('control') >= 0 ? 'control' : (variants[0] || 'control');
299
+ console.log('[cascayd-shopify] 🔄 Using fallback variant:', variantId, 'from available:', variants);
300
+ persistVariantChoice(experimentId, variantId);
301
+ }
302
+ } else {
303
+ console.error('[cascayd-shopify] ❌ No assignVariant function available');
304
+ // This shouldn't happen since waitForSDK always returns functions (direct API or SDK)
305
+ }
306
+ } else {
307
+ console.log('[cascayd-shopify] ✅ Using existing variant choice:', variantId);
308
+ }
309
+
310
+ // Show the selected variant, hide all others
311
+ console.log('[cascayd-shopify] đŸ‘ī¸ Showing/hiding variants. Selected:', variantId);
312
+ let hasRecorded = false;
313
+ variantElements.forEach(function(el, index) {
314
+ const elVariantId = el.getAttribute('data-cascayd-variant');
315
+ console.log('[cascayd-shopify] Element ' + (index + 1) + ':', { variantId: elVariantId, matches: elVariantId === variantId });
316
+
317
+ if (elVariantId === variantId) {
318
+ console.log('[cascayd-shopify] ✅ Showing element with variant:', elVariantId);
319
+ // Remove CSS hiding and mark as visible
320
+ el.classList.remove('cascayd-hidden');
321
+ el.classList.add('cascayd-visible');
322
+ // Detect if element should be inline or block based on original display
323
+ const computedStyle = window.getComputedStyle(el);
324
+ if (computedStyle.display === 'inline' || computedStyle.display === 'inline-block') {
325
+ el.classList.add('cascayd-visible-inline');
326
+ }
327
+ el.style.display = '';
328
+ el.removeAttribute('hidden');
329
+ console.log('[cascayd-shopify] ✅ Element shown, classes:', el.className);
330
+ // Record impression only once for the shown variant
331
+ if (!hasRecorded) {
332
+ recordImpression(experimentId, variantId, sdk);
333
+ hasRecorded = true;
334
+ }
335
+ } else {
336
+ console.log('[cascayd-shopify] đŸšĢ Hiding element with variant:', elVariantId);
337
+ el.style.display = 'none';
338
+ el.setAttribute('hidden', 'hidden');
339
+ el.classList.add('cascayd-hidden');
340
+ el.classList.remove('cascayd-visible', 'cascayd-visible-inline');
341
+ }
342
+ });
343
+ console.log('[cascayd-shopify] ✅ Finished processing experiment:', experimentId);
344
+ }
345
+
346
+ // Main initialization function
347
+ async function initCascaydExperiments() {
348
+ console.log('[cascayd-shopify] đŸŽ¯ initCascaydExperiments called');
349
+ console.log('[cascayd-shopify] 📄 Document readyState:', document.readyState);
350
+
351
+ // Wait for SDK to be loaded (with timeout)
352
+ console.log('[cascayd-shopify] âŗ Waiting for SDK...');
353
+ const sdk = await waitForSDK(50, 100);
354
+ console.log('[cascayd-shopify] ✅ SDK ready:', { hasSDK: !!sdk, hasAssignVariant: !!(sdk && sdk.assignVariant), hasRecord: !!(sdk && sdk.record) });
355
+
356
+ // Find all elements with data-cascayd-experiment attribute
357
+ const allExperimentElements = document.querySelectorAll('[data-cascayd-experiment]');
358
+ console.log('[cascayd-shopify] 🔍 Found experiment elements:', allExperimentElements.length);
359
+
360
+ if (allExperimentElements.length === 0) {
361
+ console.log('[cascayd-shopify] â„šī¸ No experiment elements found, exiting');
362
+ return; // No experiments to process
363
+ }
364
+
365
+ // Log all found elements
366
+ allExperimentElements.forEach(function(el, index) {
367
+ const expId = el.getAttribute('data-cascayd-experiment');
368
+ const varId = el.getAttribute('data-cascayd-variant');
369
+ console.log('[cascayd-shopify] Element ' + (index + 1) + ':', { experimentId: expId, variantId: varId, tagName: el.tagName });
370
+ });
371
+
372
+ // Group elements by experiment ID
373
+ const experiments = new Map();
374
+
375
+ allExperimentElements.forEach(function(el) {
376
+ const experimentId = el.getAttribute('data-cascayd-experiment');
377
+ if (!experimentId) {
378
+ console.warn('[cascayd-shopify] âš ī¸ Element missing experiment ID:', el);
379
+ return;
380
+ }
381
+
382
+ if (!experiments.has(experimentId)) {
383
+ experiments.set(experimentId, []);
384
+ }
385
+ experiments.get(experimentId).push(el);
386
+ });
387
+
388
+ console.log('[cascayd-shopify] 📊 Grouped experiments:', Array.from(experiments.keys()).map(function(id) {
389
+ return { experimentId: id, variantCount: experiments.get(id).length };
390
+ }));
391
+
392
+ // Process each experiment
393
+ const promises = [];
394
+ experiments.forEach(function(variantElements, experimentId) {
395
+ console.log('[cascayd-shopify] 🚀 Processing experiment:', experimentId, 'with', variantElements.length, 'variants');
396
+ promises.push(processExperiment(experimentId, variantElements, sdk));
397
+ });
398
+
399
+ console.log('[cascayd-shopify] âŗ Waiting for all experiments to process...');
400
+ await Promise.all(promises);
401
+ console.log('[cascayd-shopify] ✅ All experiments processed!');
402
+ }
403
+
404
+ // Initialize when DOM is ready
405
+ console.log('[cascayd-shopify] 🔄 Setting up initialization...');
406
+ if (document.readyState === 'loading') {
407
+ console.log('[cascayd-shopify] âŗ DOM still loading, waiting for DOMContentLoaded...');
408
+ document.addEventListener('DOMContentLoaded', function() {
409
+ console.log('[cascayd-shopify] ✅ DOMContentLoaded fired!');
410
+ initCascaydExperiments();
411
+ });
412
+ } else {
413
+ // DOM already ready
414
+ console.log('[cascayd-shopify] ✅ DOM already ready, initializing immediately');
415
+ initCascaydExperiments();
416
+ }
417
+
418
+ // Handle Shopify's dynamic content (sections loaded via AJAX)
419
+ if (typeof window.addEventListener !== 'undefined') {
420
+ console.log('[cascayd-shopify] 🛒 Setting up Shopify section event listeners...');
421
+ document.addEventListener('shopify:section:load', function() {
422
+ console.log('[cascayd-shopify] 🔄 shopify:section:load event fired, re-initializing...');
423
+ initCascaydExperiments();
424
+ });
425
+ document.addEventListener('shopify:section:reorder', function() {
426
+ console.log('[cascayd-shopify] 🔄 shopify:section:reorder event fired, re-initializing...');
427
+ initCascaydExperiments();
428
+ });
429
+ }
430
+
431
+ console.log('[cascayd-shopify] ✅ Handler script initialization complete');
432
+ })();
433
+
package/dist/types.d.ts CHANGED
@@ -21,4 +21,5 @@ export type RecordOptions = {
21
21
  experimentId?: string;
22
22
  value?: number;
23
23
  variantId?: string;
24
+ route?: string;
24
25
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cascayd/experiment",
3
- "version": "0.3.11",
3
+ "version": "0.3.14",
4
4
  "description": "A lightweight A/B testing SDK for React applications with server-side analytics integration",
5
5
  "keywords": [
6
6
  "ab-testing",