@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
JavaScript
'use client';
;
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