RocketHooks: Pro Logging With Vite Optimization

by Ahmed Latif 48 views

Hey guys! Let's dive into how we can seriously level up our logging system in RocketHooks. We're talking about ditching those repetitive process.env.NODE_ENV checks and moving to a professional setup that not only boosts our developer experience (DX) but also optimizes our production builds. This means cleaner code, better performance, and a more robust application. Buckle up; it's gonna be a fun ride!

๐Ÿ“‹ The Big Picture: Overview

Our main goal here is to replace the current, somewhat clunky, manual way of handling console logs with a sleek, automated system. Think about it: no more worrying about accidentally shipping those debug statements to production! We're aiming for a solution that:

  • Automatically removes console statements in production builds.
  • Provides a way better experience for us developers.
  • Gives us structured logging that's easy to manage.

Current Pain Points

Right now, we're dealing with:

  • Repetitive process.env.NODE_ENV === 'development' checks: It's like Groundhog Day, but with more code.
  • Manual management of console statements: Who has time for that?
  • No centralized logging strategy: It's a bit like the Wild West out there.
  • Risk of shipping console logs to production: Oops! ๐Ÿ™ˆ

Our Awesome Solution

We're going for a hybrid approach that combines the best of both worlds:

  1. vite-plugin-remove-console: This bad boy will automatically strip out console statements in production. ๐Ÿš€
  2. Custom logger utility: For a smoother DX and structured logging that makes sense.
  3. TypeScript support: Because type-safe logging is the way to go. ๐Ÿ˜Ž

๐Ÿ—๏ธ Let's Build It: Implementation Plan

We're breaking this down into phases so it's super manageable. Let's get started!

Phase 1: Install Dependencies

First up, we need to add vite-plugin-remove-console to our project. Pop open your terminal and run:

yarn add -D vite-plugin-remove-console

Package Deets:

Phase 2: Tweak the Vite Config

Now, let's tell Vite about our new plugin. Open up vite.config.ts and make it look something like this:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import removeConsole from 'vite-plugin-remove-console'
import path from 'path'

export default defineConfig(({ mode }) => ({
  plugins: [
    react(),
    tailwindcss(),
    // Only remove console in production, keep errors and warnings
    mode === 'production' && removeConsole({
      exclude: ['error', 'warn']
    }),
  ].filter(Boolean),
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 3000,
    open: true,
  },
  build: {
    target: 'esnext',
    sourcemap: true,
  },
}))

What's going on here?

  • We're importing removeConsole from our new package.
  • We're adding it to the plugins array, but only in production mode. This is key! ๐Ÿ”‘
  • We're telling it to exclude error and warn logs. We still want those in production, right?

Phase 3: Craft Our Logger Utility

This is where the magic happens. We're going to create a custom logger that's super flexible and easy to use. Create a file called src/utils/logger.ts and paste this in:

/**
 * Professional logging utility for RocketHooks application
 *
 * Features:
 * - Automatic removal in production builds via vite-plugin-remove-console
 * - Namespaced logging for better organization
 * - Type-safe logging methods
 * - Performance tracking utilities
 * - Structured data logging
 */

// Check if we're in development mode using Vite's import.meta
const isDev = import.meta.env.DEV;
const isProd = import.meta.env.PROD;

// Color codes for different log levels (browser console)
const LogColors = {
  DEBUG: 'color: #9CA3AF',
  INFO: 'color: #3B82F6',
  SUCCESS: 'color: #10B981',
  WARNING: 'color: #F59E0B',
  ERROR: 'color: #EF4444',
} as const;

interface LoggerOptions {
  namespace?: string;
  showTimestamp?: boolean;
  collapsed?: boolean;
}

class Logger {
  private namespace: string;
  private showTimestamp: boolean;

  constructor(options: LoggerOptions = {}) {
    this.namespace = options.namespace || '';
    this.showTimestamp = options.showTimestamp ?? isDev;
  }

  private format(level: string, message: string): string {
    const timestamp = this.showTimestamp
      ? `[${new Date().toISOString().split('T')[1].slice(0, -1)}]`
      : '';
    const ns = this.namespace ? `[${this.namespace}]` : '';
    return `${timestamp}${ns} ${message}`;
  }

  // Core logging methods - will be removed in production by vite-plugin-remove-console
  log(message: string, ...args: any[]): void {
    console.log(this.format('LOG', message), ...args);
  }

  debug(message: string, ...args: any[]): void {
    if (isDev) {
      console.debug(`%c${this.format('DEBUG', message)}`, LogColors.DEBUG, ...args);
    }
  }

  info(message: string, ...args: any[]): void {
    console.info(`%c${this.format('INFO', message)}`, LogColors.INFO, ...args);
  }

  success(message: string, ...args: any[]): void {
    console.log(`%c${this.format('SUCCESS', message)}`, LogColors.SUCCESS, ...args);
  }

  // These will remain in production (configured in vite.config.ts)
  warn(message: string, ...args: any[]): void {
    console.warn(`%c${this.format('WARN', message)}`, LogColors.WARNING, ...args);
  }

  error(message: string, error?: Error | unknown, ...args: any[]): void {
    console.error(`%c${this.format('ERROR', message)}`, LogColors.ERROR, error, ...args);

    // Optionally send to error tracking service in production
    if (isProd && error instanceof Error) {
      // TODO: Send to Sentry/LogRocket/etc
      // errorTracker.captureException(error);
    }
  }

  // Utility methods
  table(data: any, columns?: string[]): void {
    if (isDev) {
      console.log(this.format('TABLE', ''))
      console.table(data, columns);
    }
  }

  group(label: string, collapsed: boolean = false): void {
    const formatted = this.format('GROUP', label);
    if (collapsed) {
      console.groupCollapsed(formatted);
    } else {
      console.group(formatted);
    }
  }

  groupEnd(): void {
    console.groupEnd();
  }

  time(label: string): void {
    if (isDev) {
      console.time(this.format('TIMER', label));
    }
  }

  timeEnd(label: string): void {
    if (isDev) {
      console.timeEnd(this.format('TIMER', label));
    }
  }

  // Performance tracking
  measure<T>(label: string, fn: () => T): T {
    if (!isDev) return fn();

    const start = performance.now();
    const result = fn();
    const duration = performance.now() - start;

    this.debug(`${label} took ${duration.toFixed(2)}ms`);
    return result;
  }

  async measureAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
    if (!isDev) return fn();

    const start = performance.now();
    const result = await fn();
    const duration = performance.now() - start;

    this.debug(`${label} took ${duration.toFixed(2)}ms`);
    return result;
  }

  // Create a child logger with namespace
  child(namespace: string): Logger {
    const childNamespace = this.namespace
      ? `${this.namespace}:${namespace}`
      : namespace;

    return new Logger({
      namespace: childNamespace,
      showTimestamp: this.showTimestamp,
    });
  }
}

// Main logger instance
export const logger = new Logger();

// Pre-configured namespaced loggers for common modules
export const loggers = {
  onboarding: new Logger({ namespace: 'Onboarding' }),
  auth: new Logger({ namespace: 'Auth' }),
  api: new Logger({ namespace: 'API' }),
  graphql: new Logger({ namespace: 'GraphQL' }),
  store: new Logger({ namespace: 'Store' }),
  router: new Logger({ namespace: 'Router' }),
  ui: new Logger({ namespace: 'UI' }),
  performance: new Logger({ namespace: 'Performance' }),
  validation: new Logger({ namespace: 'Validation' }),
} as const;

// Convenience exports
export default logger;

// Type exports for better IDE support
export type { Logger, LoggerOptions };

Whoa, that's a lot of code! Let's break it down:

  • Namespaces: We can create loggers for specific parts of our app (like "Auth" or "API"). This keeps things organized.
  • Colored output: Because who doesn't love a colorful console? ๐ŸŒˆ
  • Different log levels: debug, info, warn, error, and more!
  • Performance tracking: We can easily measure how long functions take to run. โฑ๏ธ
  • TypeScript awesomeness: Type safety for all the things!
  • Automatic removal in production: Thanks to vite-plugin-remove-console, console.log and console.debug statements will disappear in production builds. ๐ŸŽ‰
  • Error tracking integration: A placeholder for sending errors to services like Sentry or LogRocket.

This logger utility is designed to give us maximum flexibility and control over our logging. With features like namespaced logging, performance tracking, and structured data logging, we can create a logging strategy that truly enhances our development workflow. The automatic removal of logs in production ensures that our builds are optimized for performance and security, while the type-safe methods provide a robust and reliable logging experience. The integration with Vite's environment variables makes it seamless to switch between development and production modes, and the potential for extension with error tracking services makes this utility a solid foundation for our application's observability strategy.

Phase 4: TypeScript Declarations

To make TypeScript happy, we need to add some declarations. Open or create src/types/global.d.ts and add this:

/// <reference types="vite/client" />

declare global {
  // Vite environment variables
  interface ImportMetaEnv {
    readonly VITE_APP_TITLE: string;
    readonly VITE_GRAPHQL_URL: string;
    readonly VITE_CLERK_PUBLISHABLE_KEY: string;
    // Add other env variables as needed
  }

  interface ImportMeta {
    readonly env: ImportMetaEnv;
  }
}

export {};

This tells TypeScript about the import.meta.env variables that Vite provides. Super important!

Phase 5: Let's Migrate! (Examples)

Okay, time to start using our fancy new logger. Let's look at some examples.

Before (The Old Way):

// In src/store/onboarding/machine.ts
if (process.env.NODE_ENV === 'development') {
  console.warn(
    `[Onboarding] No transition found for event ${event} in state ${currentState}`
  );
}

After (The New Hotness):

// In src/store/onboarding/machine.ts
import { loggers } from '@/utils/logger';

const logger = loggers.onboarding;

// This will automatically be removed in production
logger.warn(`No transition found for event ${event} in state ${currentState}`);

See how much cleaner that is? No more process.env.NODE_ENV checks! ๐ŸŽ‰

Advanced Usage Examples:

Our new logger is packed with features. Let's check out some advanced use cases.

// Performance tracking
const result = logger.measure('Heavy computation', () => {
  return performHeavyComputation();
});

// Grouped logging
logger.group('User Session Started', true);
logger.info('User ID:', userId);
logger.info('Organization:', organizationId);
logger.debug('Session metadata:', metadata);
logger.groupEnd();

// Async operations
await logger.measureAsync('API Call', async () => {
  return await fetchUserData();
});

// Child loggers for components
const componentLogger = loggers.ui.child('Button');
componentLogger.debug('Rendered with props:', props);

// Error tracking with context
try {
  await riskyOperation();
} catch (error) {
  logger.error('Failed to perform operation', error, {
    userId,
    context: getCurrentContext(),
  });
}

From measuring performance to grouping logs and tracking errors with context, our logger is equipped to handle a wide array of logging needs. The ability to create child loggers for components is particularly useful for maintaining a clear and organized logging structure as our application grows. Error tracking with context allows us to capture detailed information about the state of our application when errors occur, making debugging significantly easier. The comprehensive feature set of our new logger ensures that we have the tools we need to create a robust and informative logging system.

๐Ÿ“ Migration Checklist: Time to Get Organized

Okay, let's make a plan to migrate our existing code. Here's a checklist to keep us on track.

Files to Update:

  • [ ] vite.config.ts - Add vite-plugin-remove-console
  • [ ] src/utils/logger.ts - Create logger utility
  • [ ] src/types/global.d.ts - Add TypeScript declarations
  • [ ] package.json - Add vite-plugin-remove-console dependency

Modules to Migrate (Priority Order):

  1. High Priority (Most console.log usage):
    • [ ] src/store/onboarding/machine.ts
    • [ ] src/store/onboarding/transitions.ts
    • [ ] src/hooks/useOnboarding.ts
    • [ ] src/store/onboarding/hooks.ts
  2. Medium Priority:
    • [ ] Auth-related modules
    • [ ] API/GraphQL modules
    • [ ] Route guards
  3. Low Priority:
    • [ ] UI components
    • [ ] Utility functions

๐ŸŽฏ What Success Looks Like

  • [ ] No more process.env.NODE_ENV checks for console logging. Hallelujah!
  • [ ] All console.log/debug statements gone from the production bundle. ๐Ÿ‘ป
  • [ ] console.error and console.warn still kicking in production. ๐Ÿ’ช
  • [ ] Type-safe logging throughout the app. โœ…
  • [ ] Improved DX with namespaced, colored logging. โœจ
  • [ ] Bundle size reduction in production. ๐Ÿ“ฆ
  • [ ] Performance tracking capabilities added. ๐Ÿš€
  • [ ] No breaking changes to existing functionality. ๐Ÿ˜Œ

๐Ÿ”ง Let's Test This Thing: Testing Plan

Testing is crucial to ensure our new logging system works seamlessly across different environments. We need to verify that our development experience is enhanced, production builds are optimized, and all logging functionalities perform as expected. Hereโ€™s a comprehensive testing plan to guide us:

  1. Development Mode:

    • Verify all logging appears in the console: Ensure that all log statements, including debug, info, warn, and error messages, are correctly displayed in the console during development.
    • Check namespace formatting: Confirm that the namespace prefixes are correctly applied to log messages, making it easier to trace logs back to their origin.
    • Test performance tracking utilities: Use the logger.measure and logger.measureAsync methods to track the execution time of functions and verify that the performance metrics are accurately logged.
    • Verify colored output: Ensure that the colored output for different log levels (debug, info, warn, error) is displayed correctly in the console, enhancing readability.
  2. Production Build:

    • Build with yarn build: Create a production build of the application using the yarn build command.
    • Verify console.log/debug removed from bundle: Inspect the bundled JavaScript files to confirm that all console.log and debug statements have been removed by vite-plugin-remove-console.
    • Confirm console.error/warn still present: Ensure that console.error and console.warn statements are still included in the production bundle, as these are important for monitoring application health.
    • Check bundle size reduction: Compare the bundle size of the production build with and without the logger to quantify the reduction achieved by removing debug logs.
    • Test error tracking integration (if applicable): If integrated with an error tracking service like Sentry or LogRocket, trigger error conditions and verify that errors are correctly captured and reported by the service.
  3. Migration Testing:

    • Test each migrated module: After migrating a module to use the new logger, thoroughly test the module to ensure that all logging functionalities are working as expected.
    • Verify no functional regressions: Confirm that the changes made to implement the new logger have not introduced any regressions or broken existing functionality.
    • Check TypeScript types: Ensure that all TypeScript types are correctly handled and that there are no type-related issues after the migration.

๐Ÿ“š Don't Forget the Docs: Documentation Updates

Good documentation is essential for making our logging system easy to understand and use. This ensures consistency across the team and helps new members get up to speed quickly. Here are the key areas we need to update in our documentation:

  • Update contributing guide with logging guidelines:
    • Add a section to the contributing guide that outlines the best practices for using the new logging system.
    • Explain the importance of using appropriate log levels (debug, info, warn, error) and provide examples of when to use each level.
    • Describe how to use namespaces to organize logs and make them easier to filter and analyze.
    • Emphasize the importance of including relevant context in log messages to aid in debugging.
  • Add logger utility to developer documentation:
    • Create a dedicated section in the developer documentation that describes the logger utility in detail.
    • Explain the purpose of the logger and its key features, such as automatic log removal in production, colored output, and performance tracking.
    • Provide a comprehensive API reference for the logger, including descriptions of all methods and options.
  • Create examples for common logging patterns:
    • Include examples of common logging scenarios, such as logging errors, tracking performance, and debugging complex logic.
    • Show how to use the logger in different parts of the application, such as components, services, and event handlers.
    • Provide examples of using child loggers for namespaced logging in components and modules.
  • Document environment-specific behavior:
    • Clearly document how the logging system behaves in different environments (development, production, testing).
    • Explain how vite-plugin-remove-console automatically removes console.log and console.debug statements in production builds.
    • Describe how console.warn and console.error statements are preserved in production for critical error reporting.

By keeping our documentation up-to-date, we ensure that our team can effectively use the new logging system, leading to more efficient development and debugging processes.

๐Ÿ”— Helpful Links: References

๐Ÿ’ก Bonus Round: Additional Considerations

  1. Error Tracking Integration: We should think about hooking this up to Sentry or LogRocket for production error tracking. Super helpful!
  2. Log Levels: We could add configurable log levels (DEBUG, INFO, WARN, ERROR) for even more control.
  3. Remote Logging: For those tricky production bugs, remote logging could be a lifesaver.
  4. Performance Monitoring: Our logger could send performance metrics to monitoring services. ๐Ÿ“ˆ

๐Ÿš€ The Perks: Benefits

  • DX Improvement: Logging is now way more pleasant with namespaces and colors. ๐ŸŽ‰
  • Performance: Debug code vanishes in production. ๐Ÿ’จ
  • Type Safety: TypeScript keeps us honest. ๐Ÿค“
  • Maintainability: Centralized logging config FTW. ๐Ÿ†
  • Flexibility: We can easily add new features. ๐Ÿ’ช
  • Production Safety: No accidental console logs escaping into the wild. ๐Ÿป

Estimated Effort: 4-6 hours Priority: Medium-High Breaking Changes: None Dependencies: vite-plugin-remove-console

/cc @adnene