@keak/sdk 2.1.1 → 2.1.3

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/index.js CHANGED
@@ -1616,138 +1616,15 @@ var KeakToolbar$1 = /*#__PURE__*/Object.freeze({
1616
1616
  KeakToolbar: KeakToolbar
1617
1617
  });
1618
1618
 
1619
- const Conversion = ({ children, type = 'custom', category, label, value, metadata = {}, disabled = false }) => {
1620
- // In production, render children without tracking
1621
- const isProduction = typeof process !== 'undefined' && process.env.NODE_ENV === 'production';
1622
- const elementRef = useRef(null);
1623
- const conversionId = useRef(`conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
1624
- useEffect(() => {
1625
- // Skip tracking in production
1626
- if (isProduction || disabled)
1627
- return;
1628
- // Track conversion element exposure (event tracking is disabled in SDK, this is a no-op)
1629
- keak.track('conversion_exposure', {
1630
- conversionId: conversionId.current,
1631
- type,
1632
- category,
1633
- label,
1634
- metadata: {
1635
- ...metadata,
1636
- timestamp: new Date().toISOString(),
1637
- url: window.location.href,
1638
- referrer: document.referrer
1639
- }
1640
- });
1641
- }, [isProduction, type, category, label, metadata, disabled]);
1642
- const handleClick = (event) => {
1643
- if (disabled || isProduction)
1644
- return;
1645
- // Extract additional context from the clicked element
1646
- event.currentTarget;
1647
- const clickedElement = event.target;
1648
- // Determine the actual clicked element type and content
1649
- let elementType = type;
1650
- let elementText = '';
1651
- let elementTag = '';
1652
- let href = '';
1653
- if (clickedElement) {
1654
- elementTag = clickedElement.tagName.toLowerCase();
1655
- elementText = clickedElement.textContent?.trim() || clickedElement.getAttribute('value') || clickedElement.getAttribute('alt') || '';
1656
- // Auto-detect element type if not specified
1657
- if (type === 'custom') {
1658
- if (elementTag === 'button' || clickedElement.getAttribute('role') === 'button') {
1659
- elementType = 'button';
1660
- }
1661
- else if (elementTag === 'a') {
1662
- elementType = 'link';
1663
- href = clickedElement.getAttribute('href') || '';
1664
- }
1665
- else if (elementTag === 'input' && (clickedElement.getAttribute('type') === 'submit' || clickedElement.getAttribute('type') === 'button')) {
1666
- elementType = 'button';
1667
- }
1668
- }
1669
- // Auto-detect category based on text content if not specified
1670
- if (!category && elementText) {
1671
- const text = elementText.toLowerCase();
1672
- if (/buy|purchase|checkout|order|cart/.test(text)) {
1673
- category = 'purchase';
1674
- }
1675
- else if (/sign.?up|register|join/.test(text)) {
1676
- category = 'signup';
1677
- }
1678
- else if (/download|get.?app/.test(text)) {
1679
- category = 'download';
1680
- }
1681
- else if (/demo|trial|preview/.test(text)) {
1682
- category = 'demo';
1683
- }
1684
- else if (/contact|call|phone/.test(text)) {
1685
- category = 'contact';
1686
- }
1687
- else if (/learn.?more|read.?more/.test(text)) {
1688
- category = 'engagement';
1689
- }
1690
- }
1691
- }
1692
- // Track the conversion with variant snapshot
1693
- const conversionName = `${category || 'uncategorized'}_${elementType}`;
1694
- const conversionValue = value || 1;
1695
- const additionalData = {
1696
- conversionId: conversionId.current,
1697
- type: elementType,
1698
- category: category || 'uncategorized',
1699
- label: label || elementText || 'unlabeled',
1700
- elementDetails: {
1701
- tag: elementTag,
1702
- text: elementText,
1703
- href,
1704
- className: clickedElement.className,
1705
- id: clickedElement.id
1706
- },
1707
- ...metadata,
1708
- viewport: {
1709
- width: window.innerWidth,
1710
- height: window.innerHeight
1711
- },
1712
- scroll: {
1713
- x: window.scrollX,
1714
- y: window.scrollY
1715
- },
1716
- referrer: document.referrer
1717
- };
1718
- // Use the new snapshot-based conversion tracking
1719
- keak.trackConversion(conversionName, conversionValue, additionalData);
1720
- // Optional: Add visual feedback for debugging in development
1721
- if (process.env.NODE_ENV === 'development') {
1722
- console.log('🎯 Conversion snapshot tracked:', {
1723
- id: conversionId.current,
1724
- conversionName,
1725
- conversionValue,
1726
- type: elementType,
1727
- category: category || 'uncategorized',
1728
- label: label || elementText || 'unlabeled',
1729
- element: elementTag,
1730
- snapshotApproach: true
1731
- });
1732
- // Show brief visual feedback
1733
- const element = elementRef.current;
1734
- if (element) {
1735
- const originalTransition = element.style.transition;
1736
- element.style.transition = 'all 0.2s ease';
1737
- element.style.transform = 'scale(0.98)';
1738
- setTimeout(() => {
1739
- element.style.transform = 'scale(1)';
1740
- setTimeout(() => {
1741
- element.style.transition = originalTransition;
1742
- }, 200);
1743
- }, 100);
1744
- }
1745
- }
1746
- };
1747
- return (jsx("div", { ref: elementRef, onClick: handleClick, style: {
1619
+ /**
1620
+ * Conversion component - renders children without tracking
1621
+ * Tracking is handled by the embedded script, not the SDK
1622
+ */
1623
+ const Conversion = ({ children, type = 'custom', category, disabled = false }) => {
1624
+ return (jsx("div", { style: {
1748
1625
  display: 'contents', // This makes the wrapper invisible to CSS layout
1749
1626
  pointerEvents: disabled ? 'none' : 'auto'
1750
- }, "data-keak-conversion-id": conversionId.current, "data-keak-conversion-type": type, "data-keak-conversion-category": category, children: children }));
1627
+ }, "data-keak-conversion-type": type, "data-keak-conversion-category": category, children: children }));
1751
1628
  };
1752
1629
 
1753
1630
  function sourcePathInjectionRuntime(sourceMapping) {
@@ -2192,29 +2069,58 @@ function buildSourcePathInjection(mapping) {
2192
2069
  return `(${sourcePathInjectionRuntime.toString()})(${mappingJson});`;
2193
2070
  }
2194
2071
 
2072
+ // Hook to wait for embedded script to load (desktop projects only)
2073
+ function useWaitForEmbeddedScript() {
2074
+ const [ready, setReady] = useState(false);
2075
+ const [isDesktopProject, setIsDesktopProject] = useState(false);
2076
+ useEffect(() => {
2077
+ // Check if embedded data already exists
2078
+ if (typeof window !== 'undefined' && window.KEAK_EMBEDDED_DATA) {
2079
+ const hasProjectUuid = !!window.KEAK_EMBEDDED_DATA.projectUuid;
2080
+ setIsDesktopProject(hasProjectUuid);
2081
+ setReady(true);
2082
+ return;
2083
+ }
2084
+ // Poll for embedded data (max 500ms, check every 50ms)
2085
+ let attempts = 0;
2086
+ const maxAttempts = 10; // 10 * 50ms = 500ms
2087
+ const interval = setInterval(() => {
2088
+ attempts++;
2089
+ if (typeof window !== 'undefined' && window.KEAK_EMBEDDED_DATA) {
2090
+ const hasProjectUuid = !!window.KEAK_EMBEDDED_DATA.projectUuid;
2091
+ setIsDesktopProject(hasProjectUuid);
2092
+ setReady(true);
2093
+ clearInterval(interval);
2094
+ }
2095
+ else if (attempts >= maxAttempts) {
2096
+ // Timeout - not a desktop project or script failed to load
2097
+ setReady(true);
2098
+ setIsDesktopProject(false);
2099
+ clearInterval(interval);
2100
+ }
2101
+ }, 50);
2102
+ return () => clearInterval(interval);
2103
+ }, []);
2104
+ return { ready, isDesktopProject };
2105
+ }
2195
2106
  // Global Keak instance
2196
2107
  let keakInstance = null;
2197
2108
  let toolbarInitialized = false;
2198
2109
  class KeakSDK {
2199
2110
  constructor(config) {
2200
2111
  this.assignments = {};
2201
- this.eventQueue = [];
2202
2112
  this.initialized = false;
2203
2113
  this.liveExperiments = new Map();
2204
- this.impressions = new Map(); // experimentId -> variantName -> count
2205
2114
  this.config = config;
2206
2115
  this.userId = config.userId || this.generateUserId();
2207
2116
  this.sessionId = config.sessionId || this.generateSessionId();
2208
2117
  this.deviceId = this.generateDeviceId();
2209
- this.loadImpressions();
2210
2118
  }
2211
2119
  async init() {
2212
2120
  try {
2213
2121
  // Use empty assignments - no backend integration
2214
2122
  this.assignments = {};
2215
2123
  this.initialized = true;
2216
- // Setup automatic event tracking
2217
- this.setupAutoTracking();
2218
2124
  if (this.config.debug) {
2219
2125
  console.log('Keak SDK initialized successfully', {
2220
2126
  assignments: this.assignments,
@@ -2227,99 +2133,6 @@ class KeakSDK {
2227
2133
  this.initialized = true; // Continue with empty assignments
2228
2134
  }
2229
2135
  }
2230
- getVariant(experimentKey, availableVariants) {
2231
- // Use local assignment only
2232
- if (availableVariants && availableVariants.length > 0) {
2233
- const localAssignment = this.assignVariant(experimentKey, availableVariants);
2234
- return localAssignment;
2235
- }
2236
- // Fallback to control
2237
- return 'control';
2238
- }
2239
- trackEvent(eventName, properties = {}) {
2240
- // Event tracking disabled - dashboard only receives impression data
2241
- if (this.config.debug) {
2242
- console.log(`🔇 [Keak SDK] Event tracking disabled: ${eventName}`, properties);
2243
- }
2244
- }
2245
- trackClick(elementId, text) {
2246
- // Click tracking disabled - dashboard only receives impression data
2247
- if (this.config.debug) {
2248
- console.log(`🔇 [Keak SDK] Click tracking disabled: ${elementId}`, text);
2249
- }
2250
- }
2251
- trackConversion(conversionName, value, additionalData) {
2252
- const snapshot = this.takeVariantSnapshot(conversionName, value, additionalData);
2253
- if (this.config.debug) {
2254
- console.log(`🎯 [Keak SDK] Conversion tracked: ${conversionName}`, {
2255
- value,
2256
- snapshot: snapshot.activeVariants,
2257
- totalExperiments: Object.keys(snapshot.activeVariants).length
2258
- });
2259
- }
2260
- }
2261
- // Take a snapshot of all currently active variants when conversion happens
2262
- takeVariantSnapshot(conversionName, value, additionalData) {
2263
- const activeVariants = {};
2264
- const experimentMetadata = {};
2265
- // Capture all live experiments and their current variant assignments
2266
- this.liveExperiments.forEach((experiment, experimentId) => {
2267
- const currentVariant = this.getVariant(experimentId, experiment.variants?.map((v) => v.name));
2268
- activeVariants[experimentId] = currentVariant;
2269
- experimentMetadata[experimentId] = {
2270
- name: experiment.name,
2271
- totalVariants: experiment.totalVariants,
2272
- variants: experiment.variants,
2273
- pageUrl: experiment.pageUrl
2274
- };
2275
- });
2276
- // Capture visitor metadata at conversion time
2277
- const visitorMetadata = this.captureVisitorMetadata();
2278
- const snapshot = {
2279
- conversionId: `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
2280
- conversionName,
2281
- conversionValue: value,
2282
- userId: this.userId,
2283
- sessionId: this.sessionId,
2284
- timestamp: new Date().toISOString(),
2285
- activeVariants,
2286
- experimentMetadata,
2287
- pageUrl: window.location.href,
2288
- visitorMetadata, // NEW: Include visitor metadata
2289
- additionalData: additionalData || {}
2290
- };
2291
- // Store conversion locally as backup
2292
- this.storeConversionLocally(snapshot);
2293
- return snapshot;
2294
- }
2295
- // Store conversion snapshot locally
2296
- storeConversionLocally(snapshot) {
2297
- try {
2298
- const stored = localStorage.getItem('keak_conversions') || '[]';
2299
- const conversions = JSON.parse(stored);
2300
- conversions.push(snapshot);
2301
- // Keep only last 100 conversions to prevent storage bloat
2302
- if (conversions.length > 100) {
2303
- conversions.splice(0, conversions.length - 100);
2304
- }
2305
- localStorage.setItem('keak_conversions', JSON.stringify(conversions));
2306
- }
2307
- catch (error) {
2308
- if (this.config.debug) {
2309
- console.warn('Failed to store conversion locally:', error);
2310
- }
2311
- }
2312
- }
2313
- setupAutoTracking() {
2314
- // Auto-tracking disabled
2315
- if (this.config.debug) {
2316
- console.log('🔇 [Keak SDK] Auto-tracking disabled');
2317
- }
2318
- }
2319
- flushEvents() {
2320
- // No event queue to flush - events are disabled
2321
- this.eventQueue = [];
2322
- }
2323
2136
  generateUserId() {
2324
2137
  // Try to get existing user ID from localStorage
2325
2138
  const existing = localStorage.getItem('keak_user_id');
@@ -2342,194 +2155,16 @@ class KeakSDK {
2342
2155
  localStorage.setItem('keak_device_id', deviceId.toString());
2343
2156
  return deviceId.toString();
2344
2157
  }
2345
- // Capture visitor metadata at the moment of event
2346
- captureVisitorMetadata() {
2347
- const ua = navigator.userAgent;
2348
- const platform = navigator.platform;
2349
- // Detect OS
2350
- let os = 'Unknown';
2351
- if (/iPhone/.test(ua))
2352
- os = 'iPhone';
2353
- else if (/iPad/.test(ua))
2354
- os = 'iPad';
2355
- else if (/Android/.test(ua))
2356
- os = 'Android';
2357
- else if (platform.includes('Win'))
2358
- os = 'Windows';
2359
- else if (platform.includes('Mac'))
2360
- os = 'macOS';
2361
- else if (platform.includes('Linux'))
2362
- os = 'Linux';
2363
- else
2364
- os = platform || 'Unknown';
2365
- // Detect browser
2366
- let browser = 'Unknown';
2367
- if (ua.includes('Edg/'))
2368
- browser = 'Edge';
2369
- else if (ua.includes('Chrome') && !ua.includes('Edg'))
2370
- browser = 'Chrome';
2371
- else if (ua.includes('Safari') && !ua.includes('Chrome'))
2372
- browser = 'Safari';
2373
- else if (ua.includes('Firefox'))
2374
- browser = 'Firefox';
2375
- // Get location from localStorage cache (updated by MetricsPanel)
2376
- let country = 'Unknown';
2377
- let ipAddress = 'Unknown';
2378
- try {
2379
- const locationCache = localStorage.getItem('keak_location_cache');
2380
- if (locationCache) {
2381
- const location = JSON.parse(locationCache);
2382
- country = location.country || 'Unknown';
2383
- ipAddress = location.ipAddress || 'Unknown';
2384
- }
2385
- }
2386
- catch (error) {
2387
- // Ignore parsing errors
2388
- }
2389
- return {
2390
- location: {
2391
- country,
2392
- ipAddress
2393
- },
2394
- device: {
2395
- os,
2396
- browser,
2397
- screenSize: `${window.screen.width} × ${window.screen.height}`,
2398
- viewport: `${window.innerWidth} × ${window.innerHeight}`,
2399
- userAgent: ua
2400
- },
2401
- session: {
2402
- userId: this.userId,
2403
- deviceId: this.deviceId,
2404
- sessionId: this.sessionId,
2405
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Unknown',
2406
- language: navigator.language || 'Unknown'
2407
- }
2408
- };
2409
- }
2410
- // Simple hash function for deterministic randomization
2411
- simpleHash(str) {
2412
- let hash = 0;
2413
- for (let i = 0; i < str.length; i++) {
2414
- hash = ((hash << 5) - hash + str.charCodeAt(i)) & 0xffffffff;
2415
- }
2416
- return Math.abs(hash);
2417
- }
2418
- // Get or create variant assignment for user
2419
- assignVariant(experimentId, variants) {
2420
- const storageKey = `keak_assignment_${experimentId}`;
2421
- const stored = localStorage.getItem(storageKey);
2422
- if (stored && variants.includes(stored)) {
2423
- return stored;
2424
- }
2425
- // Deterministic randomization based on userId + experimentId
2426
- const hash = this.simpleHash(`${this.userId}_${experimentId}`);
2427
- const selectedVariant = variants[hash % variants.length];
2428
- try {
2429
- localStorage.setItem(storageKey, selectedVariant);
2430
- }
2431
- catch (error) {
2432
- if (this.config.debug) {
2433
- console.warn('Failed to store variant assignment:', error);
2434
- }
2435
- }
2436
- if (this.config.debug) {
2437
- console.log(`🎯 [Keak SDK] Assigned variant "${selectedVariant}" for experiment "${experimentId}" to user ${this.userId}`);
2438
- }
2439
- return selectedVariant;
2440
- }
2441
- // Impression tracking methods
2442
- loadImpressions() {
2443
- try {
2444
- const stored = localStorage.getItem('keak_impressions');
2445
- if (stored) {
2446
- const data = JSON.parse(stored);
2447
- this.impressions = new Map(Object.entries(data).map(([experimentId, variants]) => [
2448
- experimentId,
2449
- new Map(Object.entries(variants))
2450
- ]));
2451
- }
2452
- }
2453
- catch (error) {
2454
- if (this.config.debug) {
2455
- console.warn('Failed to load impressions from localStorage:', error);
2456
- }
2457
- }
2458
- }
2459
- saveImpressions() {
2460
- try {
2461
- const data = {};
2462
- this.impressions.forEach((variants, experimentId) => {
2463
- data[experimentId] = Object.fromEntries(variants);
2464
- });
2465
- localStorage.setItem('keak_impressions', JSON.stringify(data));
2466
- }
2467
- catch (error) {
2468
- if (this.config.debug) {
2469
- console.warn('Failed to save impressions to localStorage:', error);
2470
- }
2471
- }
2472
- }
2473
- trackImpression(experimentId, variantName) {
2474
- if (!this.impressions.has(experimentId)) {
2475
- this.impressions.set(experimentId, new Map());
2476
- }
2477
- const variants = this.impressions.get(experimentId);
2478
- const currentCount = variants.get(variantName) || 0;
2479
- variants.set(variantName, currentCount + 1);
2480
- this.saveImpressions();
2481
- // Store impression with visitor metadata
2482
- this.storeImpressionWithMetadata(experimentId, variantName);
2483
- }
2484
- storeImpressionWithMetadata(experimentId, variantName) {
2485
- try {
2486
- const visitorMetadata = this.captureVisitorMetadata();
2487
- const impressionEvent = {
2488
- experimentId,
2489
- variantName,
2490
- timestamp: new Date().toISOString(),
2491
- pageUrl: window.location.href,
2492
- visitorMetadata
2493
- };
2494
- // Store in separate detailed impressions log
2495
- const stored = localStorage.getItem('keak_impressions_detailed') || '[]';
2496
- const impressions = JSON.parse(stored);
2497
- impressions.push(impressionEvent);
2498
- // Keep only last 1000 impressions to prevent storage bloat
2499
- if (impressions.length > 1000) {
2500
- impressions.splice(0, impressions.length - 1000);
2501
- }
2502
- localStorage.setItem('keak_impressions_detailed', JSON.stringify(impressions));
2503
- }
2504
- catch (error) {
2505
- if (this.config.debug) {
2506
- console.warn('Failed to store impression metadata:', error);
2507
- }
2508
- }
2509
- }
2510
- getImpressions(experimentId) {
2511
- const variants = this.impressions.get(experimentId);
2512
- return variants ? Object.fromEntries(variants) : {};
2513
- }
2514
- getAllImpressions() {
2515
- const data = {};
2516
- this.impressions.forEach((variants, experimentId) => {
2517
- data[experimentId] = Object.fromEntries(variants);
2518
- });
2519
- return data;
2520
- }
2521
2158
  // Register live experiment
2522
2159
  registerLiveExperiment(experimentData) {
2523
- const impressions = this.getImpressions(experimentData.experimentId);
2524
2160
  this.liveExperiments.set(experimentData.experimentId, {
2525
2161
  ...experimentData,
2526
2162
  isActive: true,
2527
2163
  registeredAt: new Date().toISOString(),
2528
- currentVariant: this.assignments[experimentData.experimentId] || experimentData.variants[0]?.name || 'control',
2529
- impressions: impressions
2164
+ currentVariant: this.assignments[experimentData.experimentId] || experimentData.variants[0]?.name || 'control'
2530
2165
  });
2531
2166
  if (this.config.debug) {
2532
- console.log('📋 [Keak SDK] Live experiment registered:', experimentData.experimentId, 'Impressions:', impressions);
2167
+ console.log('📋 [Keak SDK] Live experiment registered:', experimentData.experimentId);
2533
2168
  }
2534
2169
  }
2535
2170
  // Get all live experiments for management panel
@@ -2543,7 +2178,6 @@ class KeakSDK {
2543
2178
  const experiment = this.liveExperiments.get(experimentId);
2544
2179
  if (experiment) {
2545
2180
  experiment.currentVariant = variant;
2546
- experiment.impressions = this.getImpressions(experimentId); // Update impressions
2547
2181
  this.liveExperiments.set(experimentId, experiment);
2548
2182
  }
2549
2183
  if (this.config.debug) {
@@ -2760,10 +2394,20 @@ const Experiment = ({ name, children, active = true }) => {
2760
2394
  if (typeof process !== 'undefined' && process.env.NODE_ENV === 'production') {
2761
2395
  return jsx(Fragment, { children: children });
2762
2396
  }
2763
- // Access global SDK instance directly (no context needed)
2764
- const assignments = keakInstance?.assignments || {};
2765
2397
  // Use enhanced variant detection
2766
2398
  const variantElements = detectVariantElements(children);
2399
+ // CRITICAL: Wait for embedded script to load (desktop projects only)
2400
+ const { ready, isDesktopProject } = useWaitForEmbeddedScript();
2401
+ // BLOCKING LOGIC: If desktop project and script not ready yet, return null
2402
+ // This prevents both variants from rendering and causing flicker
2403
+ if (isDesktopProject && !ready) {
2404
+ if (keakInstance?.config.debug) {
2405
+ console.log(`[Keak SDK] Waiting for embedded script for "${name}"...`);
2406
+ }
2407
+ return null; // Don't render anything until script loads
2408
+ }
2409
+ // Access global SDK instance directly (no context needed)
2410
+ const assignments = keakInstance?.assignments || {};
2767
2411
  // Extract comprehensive variant metadata
2768
2412
  const variantMetadata = variantElements.map(variant => {
2769
2413
  const variantContent = extractVariantContent(variant.props.children);
@@ -2784,7 +2428,7 @@ const Experiment = ({ name, children, active = true }) => {
2784
2428
  window.location.hostname === '127.0.0.1' ||
2785
2429
  window.location.port !== '' ||
2786
2430
  keakInstance?.config.debug);
2787
- // Register experiment with SDK for live management
2431
+ // Register experiment with SDK for live management (for toolbar)
2788
2432
  useEffect(() => {
2789
2433
  if (keakInstance && variantElements.length > 0) {
2790
2434
  // Register experiment with enhanced metadata
@@ -2805,39 +2449,10 @@ const Experiment = ({ name, children, active = true }) => {
2805
2449
  };
2806
2450
  // Register live experiment for management panel
2807
2451
  keakInstance.registerLiveExperiment(experimentData);
2808
- // Only track impressions if experiment is active
2809
- if (active) {
2810
- const variantNames = variantMetadata.map(v => v.name);
2811
- // In dev mode, don't call getVariant (which stores in localStorage)
2812
- // Instead, use "treatment" for tracking purposes
2813
- let assignedVariant;
2814
- if (isDevMode) {
2815
- assignedVariant = variantNames.includes('treatment') ? 'treatment' : variantNames[0];
2816
- if (keakInstance.config.debug) {
2817
- console.log(`🎯 [Keak SDK] Experiment "${name}" in dev mode, using "${assignedVariant}" for tracking (not stored)`);
2818
- }
2819
- }
2820
- else {
2821
- // Production mode: use normal assignment (stores in localStorage)
2822
- assignedVariant = keakInstance.getVariant(name, variantNames);
2823
- if (keakInstance.config.debug) {
2824
- console.log(`🎯 [Keak SDK] Split test assignment for "${name}": → Variant "${assignedVariant}"`);
2825
- }
2826
- }
2827
- // Track impression immediately when variant is determined
2828
- if (assignedVariant) {
2829
- keakInstance.trackImpression(name, assignedVariant);
2830
- }
2831
- }
2832
- else {
2833
- if (keakInstance.config.debug) {
2834
- console.log(`🔇 [Keak SDK] Experiment "${name}" is inactive, not tracking impressions`);
2835
- }
2836
- }
2837
2452
  }
2838
- }, [name, variantElements.length, variantMetadata, active, isDevMode]);
2453
+ }, [name, variantElements.length, variantMetadata, active]);
2839
2454
  // Get variant assignment from SDK (with proper split testing)
2840
- const variantNames = variantMetadata.map(v => v.name);
2455
+ variantMetadata.map(v => v.name);
2841
2456
  let selectedIndex;
2842
2457
  // Priority 1: Check for forced variant from setExperimentVariant (highest priority)
2843
2458
  const forcedVariant = keakInstance?.assignments[name] || assignments[name];
@@ -2850,7 +2465,37 @@ const Experiment = ({ name, children, active = true }) => {
2850
2465
  }
2851
2466
  }
2852
2467
  }
2853
- // If no forced variant, use normal logic
2468
+ // Priority 2: Check for embedded script assignment (Keak Code desktop projects)
2469
+ // This prevents flicker by using the script's computed assignment immediately
2470
+ if (selectedIndex === undefined && typeof window !== 'undefined') {
2471
+ try {
2472
+ const embeddedData = window.KEAK_EMBEDDED_DATA;
2473
+ const assignments = window.KeakAssign?.computeAssignments?.(embeddedData);
2474
+ if (assignments && embeddedData?.variants) {
2475
+ // Find test by matching selector (data-keak-id)
2476
+ const testDataKeakId = `data-keak-id="${name.replace('cro-', 'the-')}"`;
2477
+ const matchingTest = embeddedData.variants.find((v) => v.selector?.includes(testDataKeakId) || v.selector?.includes(name));
2478
+ if (matchingTest && assignments[matchingTest.testId]) {
2479
+ const embeddedVariant = assignments[matchingTest.testId]; // 'A' or 'B'
2480
+ const embeddedVariantName = embeddedVariant === 'A' ? 'control' : 'treatment';
2481
+ const embeddedIndex = variantElements.findIndex((variant) => variant.props.name === embeddedVariantName);
2482
+ if (embeddedIndex !== -1) {
2483
+ selectedIndex = embeddedIndex;
2484
+ if (keakInstance?.config.debug) {
2485
+ console.log(`📱 [Keak SDK] Using embedded script assignment for "${name}": ${embeddedVariantName} (${embeddedVariant})`);
2486
+ }
2487
+ }
2488
+ }
2489
+ }
2490
+ }
2491
+ catch (error) {
2492
+ // Silently fail - fallback to normal logic below
2493
+ if (keakInstance?.config.debug) {
2494
+ console.log(`⚠️ [Keak SDK] Could not read embedded script assignment:`, error);
2495
+ }
2496
+ }
2497
+ }
2498
+ // If no forced variant or embedded assignment, use normal logic
2854
2499
  if (selectedIndex === undefined) {
2855
2500
  if (!active) {
2856
2501
  // Inactive: Always show control variant (original)
@@ -2874,16 +2519,14 @@ const Experiment = ({ name, children, active = true }) => {
2874
2519
  }
2875
2520
  }
2876
2521
  else {
2877
- // Active in production: Normal A/B split testing
2878
- const assignedVariantName = keakInstance?.getVariant(name, variantNames) || variantNames[0];
2879
- // Find the index of the assigned variant
2880
- selectedIndex = variantElements.findIndex((variant) => variant.props.name === assignedVariantName);
2881
- // Safety fallback - if assigned variant not found, use first variant
2882
- if (selectedIndex === -1 && variantElements.length > 0) {
2883
- selectedIndex = 0;
2884
- if (keakInstance?.config.debug) {
2885
- console.warn(`⚠️ [Keak SDK] Assigned variant "${assignedVariantName}" not found, using first variant`);
2886
- }
2522
+ // Active in production: Show control (original) by default
2523
+ // Embedded script will handle A/B assignment if present
2524
+ selectedIndex = variantElements.findIndex((variant) => variant.props.name === 'control');
2525
+ if (selectedIndex === -1) {
2526
+ selectedIndex = 0; // Fallback to first variant if no "control" named variant
2527
+ }
2528
+ if (keakInstance?.config.debug) {
2529
+ console.log(`🎯 [Keak SDK] Experiment "${name}" in production, showing control (embedded script handles A/B)`);
2887
2530
  }
2888
2531
  }
2889
2532
  }
@@ -2906,6 +2549,40 @@ const Experiment = ({ name, children, active = true }) => {
2906
2549
  // Get selected variant metadata
2907
2550
  variantMetadata[currentIndex];
2908
2551
  // Clean up unused variables - toolbar positioning removed
2552
+ // Notify embedded script when variant has rendered (for Keak Code desktop projects)
2553
+ // This allows the script to skip DOM manipulation and only track impressions
2554
+ useEffect(() => {
2555
+ if (typeof window !== 'undefined' && selectedIndex !== undefined) {
2556
+ try {
2557
+ const selectedVariant = variantElements[selectedIndex];
2558
+ const variantName = selectedVariant?.props.name;
2559
+ // Check if embedded script is present
2560
+ if (window.KEAK_EMBEDDED_DATA && variantName) {
2561
+ // Notify after a brief delay to ensure React has fully rendered
2562
+ setTimeout(() => {
2563
+ // Dispatch custom event that embedded script can listen for
2564
+ window.dispatchEvent(new CustomEvent('keak-sdk-variant-rendered', {
2565
+ detail: {
2566
+ experimentId: name,
2567
+ variantName: variantName, // 'control' or 'treatment'
2568
+ variant: variantName === 'control' ? 'A' : 'B', // Map to A/B
2569
+ element: experimentRef.current
2570
+ }
2571
+ }));
2572
+ if (keakInstance?.config.debug) {
2573
+ console.log(`📢 [Keak SDK] Notified embedded script: "${name}" rendered as "${variantName}"`);
2574
+ }
2575
+ }, 0);
2576
+ }
2577
+ }
2578
+ catch (error) {
2579
+ // Silently fail - not critical
2580
+ if (keakInstance?.config.debug) {
2581
+ console.log(`⚠️ [Keak SDK] Could not notify embedded script:`, error);
2582
+ }
2583
+ }
2584
+ }
2585
+ }, [selectedIndex, name, variantElements]);
2909
2586
  if (variantElements.length === 0) {
2910
2587
  return null;
2911
2588
  }
@@ -2928,33 +2605,7 @@ const keak = {
2928
2605
  }
2929
2606
  return Promise.resolve();
2930
2607
  },
2931
- getVariant: (experimentKey, availableVariants) => {
2932
- if (!keakInstance) {
2933
- console.warn('Keak SDK not initialized. Call keak.init() first.');
2934
- return 'control';
2935
- }
2936
- return keakInstance.getVariant(experimentKey, availableVariants);
2937
- },
2938
- track: (eventName, properties) => {
2939
- // Event tracking disabled - dashboard only receives impression data
2940
- if (keakInstance?.config.debug) {
2941
- console.log(`🔇 [Keak SDK] Global event tracking disabled: ${eventName}`, properties);
2942
- }
2943
- },
2944
- trackClick: (elementId, text) => {
2945
- // Click tracking disabled - dashboard only receives impression data
2946
- if (keakInstance?.config.debug) {
2947
- console.log(`🔇 [Keak SDK] Global click tracking disabled: ${elementId}`, text);
2948
- }
2949
- },
2950
- trackConversion: (conversionName, value, additionalData) => {
2951
- if (!keakInstance) {
2952
- console.warn('Keak SDK not initialized. Call keak.init() first.');
2953
- return;
2954
- }
2955
- keakInstance.trackConversion(conversionName, value, additionalData);
2956
- },
2957
- // Live experiment management functions
2608
+ // Live experiment management functions (for toolbar)
2958
2609
  getAllLiveExperiments: () => {
2959
2610
  if (!keakInstance) {
2960
2611
  console.warn('Keak SDK not initialized. Call keak.init() first.');
@@ -2965,24 +2616,6 @@ const keak = {
2965
2616
  setExperimentVariant: (experimentId, variant) => {
2966
2617
  keakInstance?.setExperimentVariant(experimentId, variant);
2967
2618
  },
2968
- // Impression tracking methods
2969
- trackImpression: (experimentId, variantName) => {
2970
- keakInstance?.trackImpression(experimentId, variantName);
2971
- },
2972
- getImpressions: (experimentId) => {
2973
- if (!keakInstance) {
2974
- console.warn('Keak SDK not initialized. Call keak.init() first.');
2975
- return {};
2976
- }
2977
- return keakInstance.getImpressions(experimentId);
2978
- },
2979
- getAllImpressions: () => {
2980
- if (!keakInstance) {
2981
- console.warn('Keak SDK not initialized. Call keak.init() first.');
2982
- return {};
2983
- }
2984
- return keakInstance.getAllImpressions();
2985
- },
2986
2619
  };
2987
2620
  // Initialize toolbar (called by useKeak hook)
2988
2621
  function initializeToolbar(config) {