UNPKG

@loopkit/react

Version:

React TypeScript wrapper for @loopkit/javascript with built-in auto-tracking and comprehensive TypeScript support

680 lines (671 loc) 26.6 kB
'use client'; 'use strict'; var React = require('react'); var LoopKitJavaScript = require('@loopkit/javascript'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var LoopKitJavaScript__namespace = /*#__PURE__*/_interopNamespaceDefault(LoopKitJavaScript); /** * LoopKit React SDK Types * * This file extends the core @loopkit/javascript types with React-specific functionality. */ // React-specific error types class LoopKitError extends Error { constructor(message, code, originalError) { super(message); this.code = code; this.originalError = originalError; this.name = 'LoopKitError'; } } class LoopKitInitializationError extends LoopKitError { constructor(message, originalError) { super(message, 'INITIALIZATION_ERROR', originalError); this.name = 'LoopKitInitializationError'; } } class LoopKitTrackingError extends LoopKitError { constructor(message, originalError) { super(message, 'TRACKING_ERROR', originalError); this.name = 'LoopKitTrackingError'; } } // Utility function to check if we're in a browser environment const isBrowser$1 = typeof window !== 'undefined'; // Handle different export patterns from @loopkit/javascript const LoopKit$1 = LoopKitJavaScript__namespace.default || LoopKitJavaScript__namespace; // Create the context with undefined default value const LoopKitContext = React.createContext(undefined); /** * LoopKit Provider Component * * Wraps your app and provides LoopKit analytics functionality to all child components. * The underlying @loopkit/javascript SDK automatically handles page views, clicks, and errors. * * This component uses a proper SSR-compatible pattern where both server and client * render the same structure, but client-side functionality is only available after hydration. */ const LoopKitProvider = ({ apiKey, config = {}, children, onError, onInitialized, }) => { // Track if we're on the client after hydration const [isClient, setIsClient] = React.useState(false); const [isInitialized, setIsInitialized] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false); const [error, setError] = React.useState(null); const [currentConfig, setCurrentConfig] = React.useState(null); // Use refs to store callbacks to prevent dependency changes const onErrorRef = React.useRef(onError); const onInitializedRef = React.useRef(onInitialized); // Update refs when callbacks change React.useEffect(() => { onErrorRef.current = onError; }, [onError]); React.useEffect(() => { onInitializedRef.current = onInitialized; }, [onInitialized]); // Set isClient to true after hydration React.useEffect(() => { setIsClient(true); }, []); // Stabilize config object to prevent infinite loops const stableConfig = React.useMemo(() => config, [JSON.stringify(config)]); // Initialize LoopKit only on the client React.useEffect(() => { if (!isClient || !isBrowser$1) { return; } const initializeLoopKit = async () => { try { setIsLoading(true); setError(null); // Safety check for LoopKit availability if (!LoopKit$1) { throw new Error('LoopKit is not available. Check @loopkit/javascript import.'); } if (typeof LoopKit$1.init !== 'function') { throw new Error('LoopKit.init is not a function. Check @loopkit/javascript version and exports.'); } // Initialize with API key and config // The @loopkit/javascript SDK automatically handles: // - Page view tracking (enableAutoCapture: true by default) // - Click tracking (enableAutoClickTracking: true by default) // - Error tracking (enableErrorTracking: true by default) await LoopKit$1.init(apiKey, stableConfig); setCurrentConfig(LoopKit$1.getConfig()); setIsInitialized(true); setIsLoading(false); // Call success callback if (onInitializedRef.current) { onInitializedRef.current(); } } catch (err) { // Log the actual error for debugging console.error('LoopKit initialization failed:', err); console.error('Error details:', { message: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined, apiKey: apiKey ? `${apiKey.substring(0, 8)}...` : 'undefined', config: stableConfig, LoopKit: typeof LoopKit$1, LoopKitMethods: Object.keys(LoopKit$1 || {}), }); const error = new LoopKitInitializationError(`Failed to initialize LoopKit: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : new Error(String(err))); setError(error); setIsLoading(false); setIsInitialized(false); // Call error callback if (onErrorRef.current) { onErrorRef.current(error); } } }; initializeLoopKit(); }, [isClient, apiKey, stableConfig]); // Track event const track = React.useCallback(async (eventName, properties, options) => { if (!isClient || !isInitialized) { throw new LoopKitTrackingError('LoopKit is not initialized'); } try { LoopKit$1.track(eventName, properties, options); } catch (err) { const error = new LoopKitTrackingError(`Failed to track event: ${eventName}`, err instanceof Error ? err : new Error(String(err))); if (onErrorRef.current) { onErrorRef.current(error); } throw error; } }, [isClient, isInitialized]); // Track batch events const trackBatch = React.useCallback(async (events) => { if (!isClient || !isInitialized) { throw new LoopKitTrackingError('LoopKit is not initialized'); } try { LoopKit$1.trackBatch(events); } catch (err) { const error = new LoopKitTrackingError('Failed to track batch events', err instanceof Error ? err : new Error(String(err))); if (onErrorRef.current) { onErrorRef.current(error); } throw error; } }, [isClient, isInitialized]); // Identify user const identify = React.useCallback(async (userId, properties) => { if (!isClient || !isInitialized) { throw new LoopKitTrackingError('LoopKit is not initialized'); } try { LoopKit$1.identify(userId, properties); } catch (err) { const error = new LoopKitTrackingError(`Failed to identify user: ${userId}`, err instanceof Error ? err : new Error(String(err))); if (onErrorRef.current) { onErrorRef.current(error); } throw error; } }, [isClient, isInitialized]); // Group user const group = React.useCallback(async (groupId, properties, groupType) => { if (!isClient || !isInitialized) { throw new LoopKitTrackingError('LoopKit is not initialized'); } try { LoopKit$1.group(groupId, properties, groupType); } catch (err) { const error = new LoopKitTrackingError(`Failed to set group: ${groupId}`, err instanceof Error ? err : new Error(String(err))); if (onErrorRef.current) { onErrorRef.current(error); } throw error; } }, [isClient, isInitialized]); // Flush events const flush = React.useCallback(async () => { if (!isClient || !isInitialized) { throw new LoopKitTrackingError('LoopKit is not initialized'); } try { await LoopKit$1.flush(); } catch (err) { const error = new LoopKitTrackingError('Failed to flush events', err instanceof Error ? err : new Error(String(err))); if (onErrorRef.current) { onErrorRef.current(error); } throw error; } }, [isClient, isInitialized]); // Get queue size const getQueueSize = React.useCallback(() => { if (!isClient || !isInitialized) { return 0; } try { return LoopKit$1.getQueueSize(); } catch (err) { if (onErrorRef.current) { onErrorRef.current(err instanceof Error ? err : new Error(String(err))); } return 0; } }, [isClient, isInitialized]); // Configure LoopKit const configure = React.useCallback((options) => { if (!isClient || !isInitialized) { throw new LoopKitTrackingError('LoopKit is not initialized'); } try { LoopKit$1.configure(options); setCurrentConfig(LoopKit$1.getConfig()); } catch (err) { const error = new LoopKitTrackingError('Failed to configure LoopKit', err instanceof Error ? err : new Error(String(err))); if (onErrorRef.current) { onErrorRef.current(error); } throw error; } }, [isClient, isInitialized]); const contextValue = { isInitialized: isClient && isInitialized, isLoading: isClient && isLoading, error, config: currentConfig, track, trackBatch, identify, group, flush, getQueueSize, configure, }; return (React.createElement(LoopKitContext.Provider, { value: contextValue }, children)); }; /** * Hook to access LoopKit context * * @throws {Error} If used outside of LoopKitProvider */ const useLoopKitContext = () => { const context = React.useContext(LoopKitContext); if (context === undefined) { throw new Error('useLoopKitContext must be used within a LoopKitProvider. ' + "If you're using Next.js App Router, make sure your component that uses LoopKit hooks " + 'is a Client Component (add "use client" at the top) or wrapped in a Client Component.'); } return context; }; // Handle different export patterns from @loopkit/javascript const LoopKit = LoopKitJavaScript__namespace.default || LoopKitJavaScript__namespace; /** * React Error Boundary with automatic error tracking * * This component catches React component errors and automatically tracks them * with LoopKit, complementing the JavaScript SDK's global error tracking. */ class LoopKitErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null, errorInfo: null }; } static getDerivedStateFromError(error) { return { hasError: true, error, errorInfo: null, }; } componentDidCatch(error, errorInfo) { this.setState({ error, errorInfo, }); // Track the error with LoopKit if tracking is enabled if (this.props.enableTracking !== false) { try { LoopKit.track('react_error_boundary', { error_message: error.message, error_name: error.name, error_stack: error.stack, component_stack: errorInfo.componentStack, error_boundary: true, // Include page context page: typeof window !== 'undefined' ? window.location.pathname : undefined, url: typeof window !== 'undefined' ? window.location.href : undefined, }); } catch (trackingError) { console.error('Failed to track React error:', trackingError); } } // Call custom error handler if provided if (this.props.onError) { this.props.onError(error, errorInfo); } } render() { var _a, _b; if (this.state.hasError) { // Render custom fallback UI if provided if (this.props.fallback) { if (typeof this.props.fallback === 'function') { return this.props.fallback(this.state.error, this.state.errorInfo); } return this.props.fallback; } // Default fallback UI return (React.createElement("div", { style: { padding: '20px', margin: '20px', border: '1px solid #ff6b6b', borderRadius: '4px', backgroundColor: '#ffe0e0', color: '#d63031', } }, React.createElement("h2", null, "Oops! Something went wrong"), React.createElement("p", null, "We're sorry, but something unexpected happened. The error has been reported."), React.createElement("details", { style: { marginTop: '10px' } }, React.createElement("summary", null, "Error details"), React.createElement("pre", { style: { fontSize: '12px', marginTop: '10px', padding: '10px', backgroundColor: '#fff', border: '1px solid #ddd', borderRadius: '4px', overflow: 'auto', } }, (_a = this.state.error) === null || _a === void 0 ? void 0 : _a.toString(), (_b = this.state.errorInfo) === null || _b === void 0 ? void 0 : _b.componentStack)))); } return this.props.children; } } /** * Higher-order component to wrap components with error boundary */ function withErrorBoundary(WrappedComponent, errorBoundaryProps) { const WithErrorBoundaryComponent = (props) => (React.createElement(LoopKitErrorBoundary, { ...errorBoundaryProps }, React.createElement(WrappedComponent, { ...props }))); WithErrorBoundaryComponent.displayName = `withErrorBoundary(${WrappedComponent.displayName || WrappedComponent.name})`; return WithErrorBoundaryComponent; } // Utility function to check if we're in a browser environment const isBrowser = typeof window !== 'undefined'; /** * Main hook for using LoopKit analytics * * Provides enhanced functionality with convenience methods for common React tracking scenarios. * The underlying @loopkit/javascript SDK automatically handles clicks, page views, and errors. * * @param options Configuration options for the hook * @returns Enhanced LoopKit functionality with convenience methods */ const useLoopKit = (options = {}) => { const { userId, userProperties, autoIdentify = false } = options; const context = useLoopKitContext(); const { isInitialized, isLoading, error, config, track, trackBatch, identify, group, flush, getQueueSize, } = context; // Auto-identify user when hook is used with userId React.useEffect(() => { if (autoIdentify && userId && isInitialized) { identify(userId, userProperties).catch(console.error); } }, [autoIdentify, userId, userProperties, isInitialized, identify]); // Track page view with React-specific context // Note: The SDK already auto-tracks page views, but this provides additional React context const trackPageView = React.useCallback(async (pageName, properties = {}) => { if (!isBrowser) { // Skip tracking during SSR return; } const pageViewProperties = { page: pageName || window.location.pathname, url: window.location.href, title: document.title, referrer: document.referrer, source: 'react_manual', // Distinguish from auto-tracking ...properties, }; await track('page_view', pageViewProperties); }, [track]); // Track click events with React-specific context // Note: The SDK already auto-tracks clicks, but this provides manual control const trackClick = React.useCallback(async (elementName, properties = {}) => { const clickProperties = { element: elementName, page: window.location.pathname, source: 'react_manual', // Distinguish from auto-tracking ...properties, }; await track('click', clickProperties); }, [track]); // Track form submit events const trackFormSubmit = React.useCallback(async (formName, properties = {}) => { const formProperties = { form: formName, page: window.location.pathname, ...properties, }; await track('form_submit', formProperties); }, [track]); // Set user ID with optional properties const setUserId = React.useCallback(async (newUserId, properties) => { await identify(newUserId, properties); }, [identify]); // Set user properties for current user const setUserProperties = React.useCallback(async (properties) => { if (!userId) { throw new Error('User ID must be set before setting user properties'); } await identify(userId, properties); }, [identify, userId]); // Set group with optional properties const setGroup = React.useCallback(async (groupId, properties, groupType) => { await group(groupId, properties, groupType); }, [group]); return { // Core state isInitialized, isLoading, error, config, // Core methods track, trackBatch, identify, group, flush, getQueueSize, // React-specific convenience methods trackPageView, trackClick, trackFormSubmit, // User management shortcuts setUserId, setUserProperties, // Group management shortcuts setGroup, }; }; /** * Hook for tracking page views manually * * Note: @loopkit/javascript automatically tracks page views by default. * Use this hook when you need additional React-specific context or manual control. * * @param pageName Optional page name override * @param properties Additional properties to send with page view * @param dependencies Array of dependencies that should trigger a new page view */ const usePageView = (pageName, properties = {}, dependencies = []) => { const { trackPageView, isInitialized } = useLoopKit(); React.useEffect(() => { if (isInitialized) { trackPageView(pageName, properties).catch(console.error); } }, [isInitialized, pageName, ...dependencies]); // eslint-disable-line react-hooks/exhaustive-deps }; /** * Hook for auto-identifying users * * Automatically identifies the user when the hook is used with a userId. * * @param userId User ID to identify * @param properties User properties to set */ const useIdentify = (userId, properties) => { const { identify, isInitialized } = useLoopKit(); React.useEffect(() => { if (userId && isInitialized) { identify(userId, properties).catch(console.error); } }, [userId, properties, isInitialized, identify]); }; /** * Hook for tracking events with a simple function * * Returns a memoized tracking function that can be used in event handlers. * * @param eventName The name of the event to track * @param defaultProperties Default properties to include with every event * @returns Function to call when the event should be tracked */ const useTrackEvent = (eventName, defaultProperties = {}) => { const { track } = useLoopKit(); return React.useCallback((additionalProperties = {}) => { const properties = { ...defaultProperties, ...additionalProperties }; track(eventName, properties).catch(console.error); }, [track, eventName, defaultProperties]); }; /** * Hook for tracking component performance * * Tracks render times and component lifecycle events. * * @param componentName Name of the component being tracked * @param options Tracking options */ const usePerformanceTracking = (componentName, options = {}) => { const { track, isInitialized } = useLoopKit(); const { trackRenderTime = true, trackMounts = true, trackUnmounts = true, enabled = true, } = options; const renderStartTime = React.useRef(0); const mountTime = React.useRef(0); // Track render start time if (trackRenderTime && enabled && isInitialized && isBrowser && typeof performance !== 'undefined') { renderStartTime.current = performance.now(); } // Track component mount React.useEffect(() => { if (!enabled || !isInitialized || !isBrowser) return; const now = typeof performance !== 'undefined' ? performance.now() : Date.now(); mountTime.current = now; if (trackMounts) { track('component_mounted', { component_name: componentName, mount_time: mountTime.current, }).catch(console.error); } // Track render time after mount if (trackRenderTime && renderStartTime.current > 0) { const renderTime = mountTime.current - renderStartTime.current; track('component_render_time', { component_name: componentName, render_time_ms: renderTime, render_type: 'mount', }).catch(console.error); } // Cleanup function for unmount tracking return () => { if (trackUnmounts && isBrowser) { const unmountTime = typeof performance !== 'undefined' ? performance.now() : Date.now(); const componentLifetime = unmountTime - mountTime.current; track('component_unmounted', { component_name: componentName, unmount_time: unmountTime, component_lifetime_ms: componentLifetime, }).catch(console.error); } }; }, [ componentName, track, trackMounts, trackUnmounts, trackRenderTime, enabled, isInitialized, ]); // Track re-render time React.useEffect(() => { if (!enabled || !isInitialized || !trackRenderTime || !isBrowser) return; if (mountTime.current === 0) return; // Skip first render (mount) if (typeof performance !== 'undefined' && renderStartTime.current > 0) { const renderTime = performance.now() - renderStartTime.current; track('component_render_time', { component_name: componentName, render_time_ms: renderTime, render_type: 'update', }).catch(console.error); } }); }; /** * Hook for tracking React Router navigation * * Specifically designed to work with React Router and track route changes. * Note: @loopkit/javascript automatically tracks page views, but this provides * additional React Router specific context. * * @param routeName Optional route name override * @param routeParams Route parameters to include */ const useRouteTracking = (routeName, routeParams = {}) => { const { track, isInitialized } = useLoopKit(); const previousRoute = React.useRef(''); React.useEffect(() => { if (!isInitialized || !isBrowser) return; const currentRoute = routeName || window.location.pathname; // Only track if route actually changed if (currentRoute !== previousRoute.current) { previousRoute.current = currentRoute; track('route_change', { route: currentRoute, route_name: routeName, route_params: routeParams, page: window.location.pathname, url: window.location.href, source: 'react_router', }).catch(console.error); } }, [routeName, routeParams, track, isInitialized]); }; /** * Hook for tracking feature flag usage * * Tracks when feature flags are evaluated or used. * * @param flagName Name of the feature flag * @param flagValue Current value of the flag * @param metadata Additional metadata about the flag */ const useFeatureFlagTracking = (flagName, flagValue, metadata = {}) => { const { track, isInitialized } = useLoopKit(); React.useEffect(() => { if (!isInitialized) return; track('feature_flag_evaluated', { flag_name: flagName, flag_value: flagValue, flag_type: typeof flagValue, ...metadata, }).catch(console.error); }, [flagName, flagValue, metadata, track, isInitialized]); }; exports.LoopKit = LoopKitJavaScript__namespace; exports.LoopKitError = LoopKitError; exports.LoopKitErrorBoundary = LoopKitErrorBoundary; exports.LoopKitInitializationError = LoopKitInitializationError; exports.LoopKitProvider = LoopKitProvider; exports.LoopKitTrackingError = LoopKitTrackingError; exports.useFeatureFlagTracking = useFeatureFlagTracking; exports.useIdentify = useIdentify; exports.useLoopKit = useLoopKit; exports.useLoopKitContext = useLoopKitContext; exports.usePageView = usePageView; exports.usePerformanceTracking = usePerformanceTracking; exports.useRouteTracking = useRouteTracking; exports.useTrackEvent = useTrackEvent; exports.withErrorBoundary = withErrorBoundary; //# sourceMappingURL=index.js.map