@sleekio/sdk
Version:
Modern A/B Testing SDK for JavaScript Applications - Production-ready with graceful fallbacks
390 lines (332 loc) • 9.79 kB
JavaScript
/**
* React Hooks for Sleek SDK (JSX-free version for building)
* Provides seamless integration with React applications
*/
import {
useState,
useEffect,
useCallback,
useRef,
createContext,
useContext,
} from "react";
// Context for app-wide SDK access
const SleekContext = createContext(null);
export function SleekProvider({ children, sdk }) {
return React.createElement(SleekContext.Provider, { value: sdk }, children);
}
export function useSleekSDK() {
const sdk = useContext(SleekContext);
if (!sdk) {
throw new Error("useSleekSDK must be used within a SleekProvider");
}
return sdk;
}
// Main hook for A/B testing
export function useSleek(sleekSDK, experimentId) {
const [variant, setVariant] = useState("control");
const [config, setConfig] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const mountedRef = useRef(true);
useEffect(() => {
let mounted = true;
const loadVariant = async () => {
try {
setLoading(true);
setError(null);
const [variantName, variantConfig] = await Promise.all([
sleekSDK.getVariant(experimentId),
sleekSDK.getVariantConfig(experimentId),
]);
if (mounted && mountedRef.current) {
setVariant(variantName);
setConfig(variantConfig);
}
} catch (err) {
if (mounted && mountedRef.current) {
console.warn("Failed to load Sleek variant:", err);
setError(err.message);
setVariant("control");
setConfig({});
}
} finally {
if (mounted && mountedRef.current) {
setLoading(false);
}
}
};
loadVariant();
return () => {
mounted = false;
};
}, [sleekSDK, experimentId]);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
const track = useCallback(
(eventName, properties) => {
sleekSDK.track(experimentId, eventName, properties);
},
[sleekSDK, experimentId]
);
const trackConversion = useCallback(
(properties) => {
sleekSDK.trackConversion(experimentId, properties);
},
[sleekSDK, experimentId]
);
return {
variant,
config,
loading,
error,
track,
trackConversion,
};
}
// Convenience hook that combines SDK access and experiment
export function useSleekExperiment(experimentId) {
const sdk = useSleekSDK();
return useSleek(sdk, experimentId);
}
// Feature flag hook (alias for better semantics)
export function useFeatureFlag(flagId) {
return useSleekExperiment(flagId);
}
// Hook for multiple experiments
export function useSleekExperiments(experimentIds) {
const sdk = useSleekSDK();
const [experiments, setExperiments] = useState({});
const [loading, setLoading] = useState(true);
const [errors, setErrors] = useState({});
useEffect(() => {
let mounted = true;
const loadExperiments = async () => {
setLoading(true);
const results = {};
const experimentErrors = {};
await Promise.all(
experimentIds.map(async (experimentId) => {
try {
const [variant, config] = await Promise.all([
sdk.getVariant(experimentId),
sdk.getVariantConfig(experimentId),
]);
if (mounted) {
results[experimentId] = { variant, config };
}
} catch (error) {
if (mounted) {
experimentErrors[experimentId] = error.message;
results[experimentId] = { variant: "control", config: {} };
}
}
})
);
if (mounted) {
setExperiments(results);
setErrors(experimentErrors);
setLoading(false);
}
};
loadExperiments();
return () => {
mounted = false;
};
}, [sdk, experimentIds.join(",")]);
const track = useCallback(
(experimentId, eventName, properties) => {
sdk.track(experimentId, eventName, properties);
},
[sdk]
);
const trackConversion = useCallback(
(experimentId, properties) => {
sdk.trackConversion(experimentId, properties);
},
[sdk]
);
return {
experiments,
loading,
errors,
track,
trackConversion,
};
}
// Hook for assignment expiration monitoring
export function useSleekExpiration(experimentId) {
const sdk = useSleekSDK();
const [assignmentInfo, setAssignmentInfo] = useState(null);
const [isExpiring, setIsExpiring] = useState(false);
const [isExpired, setIsExpired] = useState(false);
useEffect(() => {
const checkExpiration = () => {
const info = sdk.getAssignmentInfo(experimentId);
setAssignmentInfo(info);
if (info) {
setIsExpired(info.is_expired);
setIsExpiring(info.expires_soon);
}
};
checkExpiration();
const interval = setInterval(checkExpiration, 60000); // Check every minute
return () => clearInterval(interval);
}, [sdk, experimentId]);
const refreshAssignment = useCallback(() => {
sdk.refreshAssignment(experimentId);
}, [sdk, experimentId]);
return {
assignmentInfo,
isExpiring,
isExpired,
refreshAssignment,
};
}
// Higher-order component for class components
export function withSleek(WrappedComponent, sleekSDK, experimentId) {
return function SleekEnhancedComponent(props) {
const sleekData = useSleek(sleekSDK, experimentId);
return React.createElement(WrappedComponent, {
...props,
sleek: sleekData,
});
};
}
// Hook for complex A/B test logic with automatic tracking
export function useAdvancedExperiment(experimentId, options = {}) {
const { variant, config, loading, track, trackConversion } =
useSleekExperiment(experimentId);
const sdk = useSleekSDK();
// Custom tracking with automatic event enrichment
const trackWithContext = useCallback(
(eventName, properties = {}) => {
track(eventName, {
...properties,
sessionId: options.sessionId || sdk.getSessionId(),
userSegment: options.userSegment,
deviceType:
typeof navigator !== "undefined" &&
/Mobile|Android|iPhone|iPad/.test(navigator.userAgent)
? "mobile"
: "desktop",
timestamp: Date.now(),
});
},
[track, options, sdk]
);
// Auto-track page views
useEffect(() => {
if (
!loading &&
variant !== "control" &&
options.autoTrackPageView !== false
) {
trackWithContext("page_view", {
page: typeof window !== "undefined" ? window.location.pathname : "",
variant,
});
}
}, [loading, variant, trackWithContext, options.autoTrackPageView]);
// Auto-track component mount
useEffect(() => {
if (!loading && options.autoTrackMount) {
trackWithContext("component_mount", {
component: options.componentName || "unknown",
variant,
});
}
}, [
loading,
trackWithContext,
options.autoTrackMount,
options.componentName,
variant,
]);
return {
variant,
config,
loading,
track: trackWithContext,
trackConversion,
isControl: variant === "control",
isVariant: (name) => variant === name,
getVariantValue: (key, defaultValue) => config[key] ?? defaultValue,
};
}
// Performance monitoring hook
export function useSleekPerformance(experimentId, options = {}) {
const sdk = useSleekSDK();
const startTimeRef = useRef(performance.now());
useEffect(() => {
const startTime = startTimeRef.current;
return () => {
const loadTime = performance.now() - startTime;
if (loadTime > (options.minTrackingTime || 100)) {
// Only track if > 100ms
sdk.track(experimentId, "performance_metric", {
loadTime: Math.round(loadTime),
componentName: options.componentName,
timestamp: Date.now(),
});
}
};
}, [sdk, experimentId, options.minTrackingTime, options.componentName]);
const trackCustomPerformance = useCallback(
(metricName, value, unit = "ms") => {
sdk.track(experimentId, "custom_performance", {
metricName,
value,
unit,
timestamp: Date.now(),
});
},
[sdk, experimentId]
);
return { trackCustomPerformance };
}
// Debug hook for development
export function useSleekDebug(experimentId) {
const sdk = useSleekSDK();
const [debugInfo, setDebugInfo] = useState({});
useEffect(() => {
if (process.env.NODE_ENV !== "development") return;
const updateDebugInfo = () => {
const assignmentInfo = sdk.getAssignmentInfo(experimentId);
const userContext = sdk.getUserContext();
setDebugInfo({
experimentId,
assignmentInfo,
userContext,
allAssignments: sdk.getAssignments(),
isAnonymous: sdk.isAnonymous(),
sessionId: sdk.getSessionId(),
timestamp: new Date().toISOString(),
});
};
updateDebugInfo();
const interval = setInterval(updateDebugInfo, 5000); // Update every 5 seconds
return () => clearInterval(interval);
}, [sdk, experimentId]);
const logDebugInfo = useCallback(() => {
console.group(`🧪 Sleek Debug: ${experimentId}`);
console.log("Assignment Info:", debugInfo.assignmentInfo);
console.log("User Context:", debugInfo.userContext);
console.log("All Assignments:", debugInfo.allAssignments);
console.groupEnd();
}, [experimentId, debugInfo]);
const forceReassign = useCallback(() => {
sdk.clearAssignments();
if (typeof window !== "undefined") {
window.location.reload();
}
}, [sdk]);
return {
debugInfo,
logDebugInfo,
forceReassign,
};
}