@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/Conversion.d.ts +4 -0
- package/dist/Conversion.d.ts.map +1 -1
- package/dist/index.cjs.js +132 -499
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -65
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +132 -499
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1616,138 +1616,15 @@ var KeakToolbar$1 = /*#__PURE__*/Object.freeze({
|
|
|
1616
1616
|
KeakToolbar: KeakToolbar
|
|
1617
1617
|
});
|
|
1618
1618
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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-
|
|
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
|
|
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
|
|
2453
|
+
}, [name, variantElements.length, variantMetadata, active]);
|
|
2839
2454
|
// Get variant assignment from SDK (with proper split testing)
|
|
2840
|
-
|
|
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
|
-
//
|
|
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:
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
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
|
-
|
|
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) {
|