@cascayd/experiment 0.3.10 → 0.3.12

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): Promise<{
4
+ export declare function assignVariant(experimentId: string, detectedVariants?: string[], route?: string, shop?: string): Promise<{
5
5
  variantId: string;
6
6
  config: ExperimentConfigResponse;
7
7
  }>;
package/dist/client.js CHANGED
@@ -71,10 +71,60 @@ function chooseByWeight(variants, experimentId) {
71
71
  console.log('[cascayd-sdk] chooseByWeight fallback to:', fallback);
72
72
  return fallback;
73
73
  }
74
- export async function assignVariant(experimentId) {
74
+ export async function assignVariant(experimentId, detectedVariants, route, shop) {
75
75
  const baseStatus = await getVariantStatus(experimentId, 'control');
76
- const weights = baseStatus.weights || {};
76
+ let weights = baseStatus.weights || {};
77
77
  console.log('[cascayd-sdk] assignVariant weights received:', weights);
78
+ // If we have detected variants, ensure they exist in the database
79
+ if (detectedVariants && detectedVariants.length > 0) {
80
+ // Check if all detected variants exist in weights
81
+ const missingVariants = detectedVariants.filter(vid => !(vid in weights));
82
+ if (missingVariants.length > 0) {
83
+ console.log('[cascayd-sdk] assignVariant: Missing variants detected, ensuring they exist:', missingVariants);
84
+ try {
85
+ // Call ensure endpoint to create missing variants
86
+ const ensureBody = {
87
+ experiment_id: experimentId,
88
+ variant_ids: detectedVariants,
89
+ route: route || (typeof window !== 'undefined' ? window.location.pathname : "/"),
90
+ };
91
+ if (shop) {
92
+ ensureBody.shop = shop;
93
+ }
94
+ const ensureRes = await fetch(`${BASE_URL}/experiments/ensure`, {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ Authorization: `Bearer ${API_KEY}`,
99
+ },
100
+ body: JSON.stringify(ensureBody),
101
+ });
102
+ if (ensureRes.ok) {
103
+ console.log('[cascayd-sdk] assignVariant: Successfully ensured variants exist');
104
+ // Fetch status again to get updated weights
105
+ const updatedStatus = await getVariantStatus(experimentId, 'control');
106
+ weights = updatedStatus.weights || {};
107
+ console.log('[cascayd-sdk] assignVariant: Updated weights after ensuring:', weights);
108
+ }
109
+ else {
110
+ console.warn('[cascayd-sdk] assignVariant: Failed to ensure variants, using fallback weights');
111
+ }
112
+ }
113
+ catch (error) {
114
+ console.error('[cascayd-sdk] assignVariant: Error ensuring variants:', error);
115
+ }
116
+ }
117
+ }
118
+ // If weights only has control and we have detected variants, use detected variants with equal weights (fallback)
119
+ if (Object.keys(weights).length === 1 && weights.control === 1 && detectedVariants && detectedVariants.length > 1) {
120
+ console.log('[cascayd-sdk] assignVariant: Only control in weights, using detected variants as fallback:', detectedVariants);
121
+ const equalWeight = 1 / detectedVariants.length;
122
+ weights = {};
123
+ detectedVariants.forEach(vid => {
124
+ weights[vid] = equalWeight;
125
+ });
126
+ console.log('[cascayd-sdk] assignVariant: Created equal weights from detected variants:', weights);
127
+ }
78
128
  console.log('[cascayd-sdk] assignVariant weights entries:', Object.entries(weights));
79
129
  const variants = Object.entries(weights).map(([id, weight]) => ({ id, weight }));
80
130
  console.log('[cascayd-sdk] assignVariant variants array:', variants);
@@ -118,11 +168,21 @@ export async function record(type, opts = {}) {
118
168
  body.experiment_id = opts.experimentId;
119
169
  const fromCookie = readVariantChoice(opts.experimentId);
120
170
  const variantId = opts.variantId || fromCookie;
121
- if (variantId)
171
+ if (variantId) {
122
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
+ }
123
177
  }
124
178
  if (typeof opts.value === 'number')
125
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
+ }
126
186
  const res = await fetch(`${BASE_URL}/events`, {
127
187
  method: 'POST',
128
188
  headers: {
@@ -0,0 +1 @@
1
+ export {};
@@ -1,433 +1,163 @@
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);
1
+ import { record, getVariantStatus } from './client';
2
+ import { getOrCreateSession, persistVariantChoice, readVariantChoice } from './cookies';
3
+ const sentImpressions = new Set();
4
+ function chooseByWeight(variants) {
5
+ const total = variants.reduce((s, v) => s + v.weight, 0);
6
+ if (total === 0)
7
+ return variants[0]?.id ?? 'control';
87
8
  const r = Math.random() * total;
88
9
  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
- }
10
+ for (const v of variants) {
11
+ acc += v.weight;
12
+ if (r <= acc)
13
+ return v.id;
94
14
  }
95
- return variants[variants.length - 1] ? variants[variants.length - 1].id : 'control';
96
- }
97
-
98
- function baseStatusToVariants(status) {
15
+ return variants[variants.length - 1]?.id ?? 'control';
16
+ }
17
+ function baseStatusToVariants(status) {
99
18
  const entries = Object.entries(status.weights || {});
100
19
  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;
20
+ return [{ id: 'control', weight: 1 }];
167
21
  }
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
- }
22
+ return entries.map(([id, weight]) => ({ id, weight }));
23
+ }
24
+ async function assignVariantForExperiment(experimentId) {
25
+ try {
26
+ const existing = readVariantChoice(experimentId);
27
+ if (existing) {
28
+ try {
29
+ const status = await getVariantStatus(experimentId, existing);
30
+ const serving = status.serving_variant_id || existing;
31
+ if (serving === existing) {
32
+ return existing;
33
+ }
34
+ }
35
+ catch (error) {
36
+ console.warn('[cascayd-shopify] Failed to verify existing variant, reassigning', error);
37
+ }
233
38
  }
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);
39
+ const baseStatus = await getVariantStatus(experimentId, 'control');
40
+ const weights = baseStatus.weights || {};
41
+ const variants = Object.entries(weights).map(([id, weight]) => ({ id, weight }));
42
+ let candidate = chooseByWeight(variants.length > 0 ? variants : baseStatusToVariants(baseStatus));
43
+ let finalStatus = null;
44
+ if (candidate) {
45
+ try {
46
+ finalStatus = await getVariantStatus(experimentId, candidate);
47
+ }
48
+ catch (error) {
49
+ console.warn('[cascayd-shopify] getVariantStatus failed, using candidate', error);
50
+ }
245
51
  }
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;
52
+ const serving = finalStatus?.serving_variant_id || candidate || 'control';
53
+ persistVariantChoice(experimentId, serving);
54
+ return serving;
257
55
  }
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);
56
+ catch (error) {
57
+ console.error('[cascayd-shopify] Variant assignment failed', error);
58
+ return 'control';
267
59
  }
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);
60
+ }
61
+ function showVariant(experimentId, variantId) {
62
+ const selector = `[data-cascayd-experiment="${experimentId}"]`;
63
+ const elements = document.querySelectorAll(selector);
64
+ elements.forEach((el) => {
65
+ const elVariantId = el.getAttribute('data-cascayd-variant');
66
+ if (elVariantId === variantId) {
67
+ el.classList.add('cascayd-visible');
68
+ const computedStyle = window.getComputedStyle(el);
69
+ if (computedStyle.display === 'inline' || computedStyle.display === 'inline-block') {
70
+ el.classList.add('cascayd-visible-inline');
296
71
  }
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
72
  }
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;
73
+ else {
74
+ el.classList.remove('cascayd-visible', 'cascayd-visible-inline');
334
75
  }
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
76
  });
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
77
+ }
78
+ function sendImpression(experimentId, variantId) {
79
+ if (!variantId)
80
+ return;
81
+ const key = `${experimentId}:${variantId}`;
82
+ if (sentImpressions.has(key))
83
+ return;
84
+ sentImpressions.add(key);
85
+ void record('impression', { experimentId, variantId });
86
+ }
87
+ function findVariantIdFromElement(el, experimentId) {
88
+ // First try cookie (most reliable)
89
+ const fromCookie = readVariantChoice(experimentId);
90
+ if (fromCookie)
91
+ return fromCookie;
92
+ // Fallback: traverse up DOM to find parent with data-cascayd-variant
93
+ let current = el;
94
+ while (current) {
95
+ const variantId = current.getAttribute('data-cascayd-variant');
96
+ if (variantId)
97
+ return variantId;
98
+ // Check if we're inside an experiment container
99
+ const experimentIdAttr = current.getAttribute('data-cascayd-experiment');
100
+ if (experimentIdAttr === experimentId) {
101
+ // We're in the right experiment container, continue up
102
+ current = current.parentElement;
103
+ }
104
+ else {
105
+ // Move up to find experiment container
106
+ current = current.parentElement;
107
+ }
363
108
  }
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 });
109
+ return null;
110
+ }
111
+ function setupConversionTracking(experimentId) {
112
+ const selector = `[data-cascayd-experiment="${experimentId}"] [data-cascayd-conversion]`;
113
+ const elements = document.querySelectorAll(selector);
114
+ elements.forEach((el) => {
115
+ if (el.hasAttribute('data-cascayd-tracked'))
116
+ return;
117
+ el.setAttribute('data-cascayd-tracked', 'true');
118
+ el.addEventListener('click', (e) => {
119
+ const variantId = findVariantIdFromElement(el, experimentId);
120
+ if (variantId) {
121
+ void record('conversion', { experimentId, variantId });
122
+ }
123
+ else {
124
+ console.warn(`[cascayd-shopify] No variant found for experiment ${experimentId} when recording conversion`);
125
+ }
126
+ });
370
127
  });
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);
128
+ }
129
+ async function processExperiment(experimentId) {
130
+ try {
131
+ getOrCreateSession();
132
+ const variantId = await assignVariantForExperiment(experimentId);
133
+ showVariant(experimentId, variantId);
134
+ sendImpression(experimentId, variantId);
135
+ setupConversionTracking(experimentId);
136
+ }
137
+ catch (error) {
138
+ console.error(`[cascayd-shopify] Failed to process experiment ${experimentId}`, error);
139
+ }
140
+ }
141
+ function initShopifyHandler() {
142
+ if (typeof window === 'undefined' || typeof document === 'undefined')
379
143
  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();
144
+ const experimentElements = document.querySelectorAll('[data-cascayd-experiment]');
145
+ const experimentIds = new Set();
146
+ experimentElements.forEach((el) => {
147
+ const experimentId = el.getAttribute('data-cascayd-experiment');
148
+ if (experimentId) {
149
+ experimentIds.add(experimentId);
150
+ }
424
151
  });
425
- document.addEventListener('shopify:section:reorder', function() {
426
- console.log('[cascayd-shopify] 🔄 shopify:section:reorder event fired, re-initializing...');
427
- initCascaydExperiments();
152
+ experimentIds.forEach((experimentId) => {
153
+ void processExperiment(experimentId);
428
154
  });
429
- }
430
-
431
- console.log('[cascayd-shopify] Handler script initialization complete');
432
- })();
433
-
155
+ }
156
+ if (typeof window !== 'undefined') {
157
+ if (document.readyState === 'loading') {
158
+ document.addEventListener('DOMContentLoaded', initShopifyHandler);
159
+ }
160
+ else {
161
+ initShopifyHandler();
162
+ }
163
+ }
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.10",
3
+ "version": "0.3.12",
4
4
  "description": "A lightweight A/B testing SDK for React applications with server-side analytics integration",
5
5
  "keywords": [
6
6
  "ab-testing",