UNPKG

@sleekio/sdk

Version:

Modern A/B Testing SDK for JavaScript Applications - Production-ready with graceful fallbacks

390 lines (332 loc) 9.79 kB
/** * 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, }; }