431 lines
11 KiB
TypeScript
431 lines
11 KiB
TypeScript
/**
|
|
* State Middleware
|
|
* FE-STATE-010: Add middleware for logging, analytics, error handling
|
|
*
|
|
* Provides a comprehensive middleware for Zustand stores that handles:
|
|
* - State change logging
|
|
* - Analytics tracking
|
|
* - Error handling and reporting
|
|
*/
|
|
|
|
import { StateCreator } from 'zustand';
|
|
import { logger } from './logger';
|
|
|
|
/**
|
|
* Analytics event types
|
|
*/
|
|
export type AnalyticsEventType =
|
|
| 'state_change'
|
|
| 'action_called'
|
|
| 'error_occurred'
|
|
| 'performance_metric';
|
|
|
|
/**
|
|
* Analytics event
|
|
*/
|
|
export interface AnalyticsEvent {
|
|
type: AnalyticsEventType;
|
|
store: string;
|
|
action?: string;
|
|
timestamp: number;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Analytics handler function
|
|
*/
|
|
export type AnalyticsHandler = (event: AnalyticsEvent) => void;
|
|
|
|
/**
|
|
* Error handler function
|
|
*/
|
|
export type ErrorHandler = (
|
|
error: Error,
|
|
context: {
|
|
store: string;
|
|
action?: string;
|
|
state?: unknown;
|
|
},
|
|
) => void;
|
|
|
|
/**
|
|
* Options for state middleware
|
|
*/
|
|
export interface StateMiddlewareOptions {
|
|
/** Store name for logging and analytics */
|
|
storeName: string;
|
|
/** Enable logging (default: true in dev, false in prod) */
|
|
enableLogging?: boolean;
|
|
/** Enable analytics tracking (default: true) */
|
|
enableAnalytics?: boolean;
|
|
/** Enable error handling (default: true) */
|
|
enableErrorHandling?: boolean;
|
|
/** Custom analytics handler */
|
|
analyticsHandler?: AnalyticsHandler;
|
|
/** Custom error handler */
|
|
errorHandler?: ErrorHandler;
|
|
/** Filter state changes to log (return false to skip) */
|
|
shouldLog?: (state: unknown, prevState: unknown) => boolean;
|
|
/** Filter actions to track (return false to skip) */
|
|
shouldTrack?: (action: string, args: unknown[]) => boolean;
|
|
/** Sanitize state for logging (remove sensitive data) */
|
|
sanitizeState?: (state: unknown) => unknown;
|
|
}
|
|
|
|
/**
|
|
* Default analytics handler (can be overridden)
|
|
*/
|
|
let defaultAnalyticsHandler: AnalyticsHandler | null = null;
|
|
|
|
/**
|
|
* Set default analytics handler
|
|
*/
|
|
export function setAnalyticsHandler(handler: AnalyticsHandler): void {
|
|
defaultAnalyticsHandler = handler;
|
|
}
|
|
|
|
/**
|
|
* Default error handler (can be overridden)
|
|
*/
|
|
let defaultErrorHandler: ErrorHandler | null = null;
|
|
|
|
/**
|
|
* Set default error handler
|
|
*/
|
|
export function setErrorHandler(handler: ErrorHandler): void {
|
|
defaultErrorHandler = handler;
|
|
}
|
|
|
|
/**
|
|
* Track analytics event
|
|
*/
|
|
function trackAnalytics(
|
|
event: Omit<AnalyticsEvent, 'timestamp'>,
|
|
handler?: AnalyticsHandler,
|
|
): void {
|
|
const analyticsHandler = handler || defaultAnalyticsHandler;
|
|
if (!analyticsHandler) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
analyticsHandler({
|
|
...event,
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
logger.error('[StateMiddleware] Analytics tracking failed', {
|
|
error: String(error),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle error
|
|
*/
|
|
function handleError(
|
|
error: Error,
|
|
context: {
|
|
store: string;
|
|
action?: string;
|
|
state?: unknown;
|
|
},
|
|
handler?: ErrorHandler,
|
|
): void {
|
|
const errorHandler = handler || defaultErrorHandler;
|
|
|
|
// Always log errors
|
|
logger.error(`[StateMiddleware] Error in ${context.store}:`, {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
action: context.action,
|
|
context,
|
|
});
|
|
|
|
// Call custom error handler if provided
|
|
if (errorHandler) {
|
|
try {
|
|
errorHandler(error, context);
|
|
} catch (handlerError) {
|
|
logger.error('[StateMiddleware] Error handler failed', {
|
|
error: String(handlerError),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Track error analytics (use default handler, not error handler)
|
|
trackAnalytics({
|
|
type: 'error_occurred',
|
|
store: context.store,
|
|
action: context.action,
|
|
metadata: {
|
|
error: error.message,
|
|
errorName: error.name,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sanitize state for logging (remove sensitive data)
|
|
*/
|
|
function sanitizeState(
|
|
state: unknown,
|
|
customSanitize?: (state: unknown) => unknown,
|
|
): unknown {
|
|
if (customSanitize) {
|
|
return customSanitize(state);
|
|
}
|
|
|
|
// Default sanitization: remove common sensitive fields
|
|
if (typeof state === 'object' && state !== null) {
|
|
const sanitized = { ...(state as Record<string, unknown>) };
|
|
const sensitiveKeys = [
|
|
'password',
|
|
'token',
|
|
'secret',
|
|
'apiKey',
|
|
'accessToken',
|
|
'refreshToken',
|
|
];
|
|
|
|
for (const key of sensitiveKeys) {
|
|
if (key in sanitized) {
|
|
sanitized[key] = '[REDACTED]';
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Zustand middleware for logging, analytics, and error handling
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* export const useMyStore = create<MyState>()(
|
|
* stateMiddleware(
|
|
* (set, get) => ({
|
|
* // store implementation
|
|
* }),
|
|
* { storeName: 'MyStore' }
|
|
* )
|
|
* );
|
|
* ```
|
|
*/
|
|
export function stateMiddleware<T extends object>(
|
|
config: StateCreator<T>,
|
|
options: StateMiddlewareOptions,
|
|
): StateCreator<T> {
|
|
const {
|
|
storeName,
|
|
enableLogging = import.meta.env.DEV,
|
|
enableAnalytics = true,
|
|
enableErrorHandling = true,
|
|
analyticsHandler,
|
|
errorHandler,
|
|
shouldLog = () => true,
|
|
shouldTrack = () => true,
|
|
sanitizeState: customSanitize,
|
|
} = options;
|
|
|
|
return (set, get, api) => {
|
|
let previousState: T | null = null;
|
|
let actionCallCount = 0;
|
|
|
|
// Wrap set function to intercept state changes
|
|
const wrappedSet: typeof set = (...args) => {
|
|
const currentState = get();
|
|
|
|
// Log state changes
|
|
if (enableLogging && shouldLog(currentState, previousState)) {
|
|
const sanitizedCurrent = sanitizeState(currentState, customSanitize);
|
|
const sanitizedPrevious = sanitizeState(previousState, customSanitize);
|
|
|
|
logger.debug(`[StateMiddleware:${storeName}] State change:`, {
|
|
previous: sanitizedPrevious,
|
|
current: sanitizedCurrent,
|
|
});
|
|
}
|
|
|
|
// Track analytics
|
|
if (enableAnalytics) {
|
|
trackAnalytics(
|
|
{
|
|
type: 'state_change',
|
|
store: storeName,
|
|
metadata: {
|
|
hasPreviousState: previousState !== null,
|
|
},
|
|
},
|
|
analyticsHandler,
|
|
);
|
|
}
|
|
|
|
// Call original set
|
|
try {
|
|
set(...args);
|
|
previousState = get();
|
|
} catch (error) {
|
|
if (enableErrorHandling) {
|
|
handleError(
|
|
error instanceof Error ? error : new Error(String(error)),
|
|
{
|
|
store: storeName,
|
|
state: currentState,
|
|
},
|
|
errorHandler,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Create store with wrapped set
|
|
const store = config(wrappedSet, get, api);
|
|
|
|
// Wrap store actions to track calls
|
|
if (enableAnalytics || enableLogging) {
|
|
const wrappedStore = { ...store } as Record<string, unknown>;
|
|
|
|
// Wrap all functions in the store
|
|
for (const [key, value] of Object.entries(wrappedStore)) {
|
|
if (typeof value === 'function' && key !== 'setState') {
|
|
const originalAction = value as (...args: unknown[]) => unknown;
|
|
|
|
wrappedStore[key] = ((...args: unknown[]) => {
|
|
actionCallCount++;
|
|
const startTime = performance.now();
|
|
|
|
// Track action call
|
|
if (enableAnalytics && shouldTrack(key, args)) {
|
|
trackAnalytics(
|
|
{
|
|
type: 'action_called',
|
|
store: storeName,
|
|
action: key,
|
|
metadata: {
|
|
callCount: actionCallCount,
|
|
argsCount: args.length,
|
|
},
|
|
},
|
|
analyticsHandler,
|
|
);
|
|
}
|
|
|
|
// Log action call
|
|
if (enableLogging) {
|
|
logger.debug(
|
|
`[StateMiddleware:${storeName}] Action called: ${key}`,
|
|
{
|
|
args: args.length > 0 ? args : undefined,
|
|
},
|
|
);
|
|
}
|
|
|
|
try {
|
|
const result = originalAction(...args);
|
|
|
|
// Track performance if it's a promise
|
|
if (result instanceof Promise) {
|
|
const endTime = performance.now();
|
|
const duration = endTime - startTime;
|
|
|
|
if (enableAnalytics) {
|
|
trackAnalytics(
|
|
{
|
|
type: 'performance_metric',
|
|
store: storeName,
|
|
action: key,
|
|
metadata: {
|
|
duration,
|
|
isAsync: true,
|
|
},
|
|
},
|
|
analyticsHandler,
|
|
);
|
|
}
|
|
|
|
// Handle promise errors
|
|
if (enableErrorHandling) {
|
|
result.catch((error) => {
|
|
handleError(
|
|
error instanceof Error ? error : new Error(String(error)),
|
|
{
|
|
store: storeName,
|
|
action: key,
|
|
state: get(),
|
|
},
|
|
errorHandler,
|
|
);
|
|
});
|
|
}
|
|
} else {
|
|
// Track synchronous action performance
|
|
const endTime = performance.now();
|
|
const duration = endTime - startTime;
|
|
|
|
if (enableAnalytics && duration > 10) {
|
|
// Only track if it takes more than 10ms
|
|
trackAnalytics(
|
|
{
|
|
type: 'performance_metric',
|
|
store: storeName,
|
|
action: key,
|
|
metadata: {
|
|
duration,
|
|
isAsync: false,
|
|
},
|
|
},
|
|
analyticsHandler,
|
|
);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
if (enableErrorHandling) {
|
|
handleError(
|
|
error instanceof Error ? error : new Error(String(error)),
|
|
{
|
|
store: storeName,
|
|
action: key,
|
|
state: get(),
|
|
},
|
|
errorHandler,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}) as typeof originalAction;
|
|
}
|
|
}
|
|
|
|
return wrappedStore as T;
|
|
}
|
|
|
|
return store;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper to create a store with state middleware
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* export const useMyStore = createWithMiddleware<MyState>(
|
|
* (set, get) => ({
|
|
* // store implementation
|
|
* }),
|
|
* { storeName: 'MyStore' }
|
|
* );
|
|
* ```
|
|
*/
|
|
export function createWithMiddleware<T extends object>(
|
|
config: StateCreator<T>,
|
|
options: StateMiddlewareOptions,
|
|
): StateCreator<T> {
|
|
return stateMiddleware(config, options);
|
|
}
|