Debugging and Development Tools

1. RxJS DevTools Browser Extension

Feature Description Use Case Browser Support
Observable Graph Visual dependency graph of observables Understand stream relationships Chrome, Firefox
Timeline View Chronological emission timeline Track emission sequences Chrome, Firefox
Value Inspector Inspect emitted values in detail Examine complex objects Chrome, Firefox
Subscription Tracking Monitor active subscriptions Detect memory leaks Chrome, Firefox
Performance Metrics Measure operator execution time Identify bottlenecks Chrome
Stream Recording Record and replay streams Reproduce bugs Chrome

Example: Enable RxJS DevTools instrumentation

// Install: npm install @rxjs-debugging/rxjs-debugging-for-vscode

// Enable in development
import { instrumentObservables } from '@rxjs-debugging/rxjs-debugging-for-vscode';

if (environment.development) {
  instrumentObservables({
    // Tag observables for easier identification
    tag: true,
    // Capture stack traces
    stackTrace: true,
    // Track subscriptions
    trackSubscriptions: true
  });
}

// Tag observables for DevTools
const user$ = this.http.get<User>('/api/user').pipe(
  tag('user-request'),  // Shows as "user-request" in DevTools
  shareReplay(1)
);

// DevTools will show:
// - Observable name: "user-request"
// - Subscriber count
// - Emission timeline
// - Values emitted
DevTools Installation:
  • Chrome: Install "RxJS DevTools" from Chrome Web Store
  • Firefox: Install "RxJS DevTools" from Firefox Add-ons
  • Setup: Instrument observables in development mode only
  • Performance: DevTools adds overhead, disable in production

2. tap Operator for Stream Debugging

Pattern Syntax Description Best Practice
Value Logging tap(val => console.log(val)) Log emitted values Use descriptive labels
Side Effect Debugging tap(() => doSomething()) Execute without modifying stream Keep side effects pure
Observer Debugging tap({ next, error, complete }) Log all notification types Track stream lifecycle
Conditional Logging tap(val => val && log(val)) Log based on conditions Reduce noise in logs
Performance Tracking tap(() => performance.mark()) Measure execution time Use Performance API

Example: Comprehensive tap debugging

// Create reusable debug tap operators
const debug = (tag: string) => <T>(source: Observable<T>) => {
  return source.pipe(
    tap({
      next: value => console.log(`[${tag}] Next:`, value),
      error: error => console.error(`[${tag}] Error:`, error),
      complete: () => console.log(`[${tag}] Complete`)
    })
  );
};

const debugTime = (tag: string) => <T>(source: Observable<T>) => {
  const startTime = performance.now();
  let emissionCount = 0;
  
  return source.pipe(
    tap({
      next: () => {
        emissionCount++;
        const elapsed = performance.now() - startTime;
        console.log(`[${tag}] Emission #${emissionCount} at ${elapsed.toFixed(2)}ms`);
      },
      complete: () => {
        const totalTime = performance.now() - startTime;
        console.log(`[${tag}] Completed: ${emissionCount} emissions in ${totalTime.toFixed(2)}ms`);
      }
    })
  );
};

// Usage in stream
this.http.get<User[]>('/api/users').pipe(
  debug('API Response'),
  map(users => users.filter(u => u.active)),
  debug('After Filter'),
  debugTime('Processing'),
  map(users => users.map(u => u.name)),
  debug('Final Result')
).subscribe();

Example: Stack trace capture with tap

// Capture stack trace at subscription point
const captureStack = (label: string) => <T>(source: Observable<T>) => {
  const stack = new Error().stack;
  
  return source.pipe(
    tap({
      error: (err) => {
        console.error(`[${label}] Error occurred`);
        console.error('Original stack:', stack);
        console.error('Error:', err);
      }
    })
  );
};

// Use to track where errors originate
const data$ = this.service.getData().pipe(
  captureStack('getData call'),
  map(data => data.value),
  captureStack('after map')
);

// When error occurs, you'll see:
// - Where the observable was created
// - Which operator failed
// - Full error context

3. Observable Inspector and Stream Visualization

Tool Description Feature Platform
RxViz Interactive marble diagram visualizer Real-time stream visualization Web (rxviz.com)
RxJS Marbles Live operator testing playground Test operator combinations Web (rxmarbles.com)
Console Utilities Custom console formatters Structured logging Browser DevTools
Performance Monitor Execution time tracking Identify slow operators Chrome DevTools
Memory Profiler Detect subscription leaks Track object retention Chrome DevTools

Example: Custom observable inspector

// Observable inspector utility
class ObservableInspector {
  private observations = new Map<string, ObservationData>();
  
  inspect<T>(name: string) {
    return (source: Observable<T>): Observable<T> => {
      const data: ObservationData = {
        name,
        emissions: [],
        subscriptions: 0,
        activeSubscriptions: 0,
        errors: [],
        completions: 0,
        startTime: Date.now()
      };
      
      this.observations.set(name, data);
      
      return new Observable<T>(subscriber => {
        data.subscriptions++;
        data.activeSubscriptions++;
        
        const subscription = source.subscribe({
          next: (value) => {
            data.emissions.push({
              value,
              timestamp: Date.now() - data.startTime
            });
            subscriber.next(value);
          },
          error: (err) => {
            data.errors.push({
              error: err,
              timestamp: Date.now() - data.startTime
            });
            data.activeSubscriptions--;
            subscriber.error(err);
          },
          complete: () => {
            data.completions++;
            data.activeSubscriptions--;
            subscriber.complete();
          }
        });
        
        return () => {
          data.activeSubscriptions--;
          subscription.unsubscribe();
        };
      });
    };
  }
  
  getReport(name?: string): string {
    if (name) {
      return this.formatReport(this.observations.get(name));
    }
    
    let report = 'Observable Inspector Report\n';
    report += '='.repeat(50) + '\n\n';
    
    this.observations.forEach((data, name) => {
      report += this.formatReport(data) + '\n\n';
    });
    
    return report;
  }
  
  private formatReport(data?: ObservationData): string {
    if (!data) return 'No data available';
    
    return `
Observable: ${data.name}
Subscriptions: ${data.subscriptions} (${data.activeSubscriptions} active)
Emissions: ${data.emissions.length}
Errors: ${data.errors.length}
Completions: ${data.completions}
Duration: ${Date.now() - data.startTime}ms

Emission Timeline:
${data.emissions.map(e => 
  `  ${e.timestamp}ms: ${JSON.stringify(e.value)}`
).join('\n')}

${data.errors.length > 0 ? 'Errors:\n' + data.errors.map(e => 
  `  ${e.timestamp}ms: ${e.error}`
).join('\n') : ''}
    `.trim();
  }
}

// Usage
const inspector = new ObservableInspector();

const data$ = interval(1000).pipe(
  take(5),
  inspector.inspect('interval-stream'),
  map(x => x * 2),
  inspector.inspect('after-map')
);

data$.subscribe();

// After completion, view report
setTimeout(() => {
  console.log(inspector.getReport());
}, 6000);

4. Console Logging Patterns for Observable Streams

Pattern Implementation Use Case Advantage
Grouped Logs console.group() Organize related emissions Hierarchical visualization
Colored Output %c CSS styling Differentiate streams Visual clarity
Table Format console.table() Display arrays/objects Structured view
Time Tracking console.time() Measure durations Performance insight
Trace Logs console.trace() Call stack visualization Track execution flow
Count Emissions console.count() Track emission frequency Detect duplicates

Example: Advanced logging patterns

// Styled console logger for observables
class ObservableLogger {
  private colors = {
    next: '#4CAF50',
    error: '#F44336',
    complete: '#2196F3',
    subscribe: '#FF9800',
    unsubscribe: '#9E9E9E'
  };
  
  log<T>(name: string, options: LogOptions = {}) {
    return (source: Observable<T>): Observable<T> => {
      const groupLabel = `🔵 Observable: ${name}`;
      
      return new Observable<T>(subscriber => {
        if (options.logSubscription) {
          console.log(
            `%c[${name}] Subscribe`,
            `color: ${this.colors.subscribe}; font-weight: bold`
          );
        }
        
        if (options.groupLogs) {
          console.group(groupLabel);
        }
        
        const startTime = performance.now();
        let emissionCount = 0;
        
        const subscription = source.subscribe({
          next: (value) => {
            emissionCount++;
            const elapsed = performance.now() - startTime;
            
            console.log(
              `%c▶ Next #${emissionCount}`,
              `color: ${this.colors.next}; font-weight: bold`,
              `(${elapsed.toFixed(2)}ms)`,
              value
            );
            
            if (options.tableFormat && typeof value === 'object') {
              console.table(value);
            }
            
            subscriber.next(value);
          },
          error: (err) => {
            const elapsed = performance.now() - startTime;
            
            console.log(
              `%c✖ Error`,
              `color: ${this.colors.error}; font-weight: bold`,
              `(${elapsed.toFixed(2)}ms)`
            );
            console.error(err);
            
            if (options.stackTrace) {
              console.trace('Error stack trace:');
            }
            
            if (options.groupLogs) {
              console.groupEnd();
            }
            
            subscriber.error(err);
          },
          complete: () => {
            const elapsed = performance.now() - startTime;
            
            console.log(
              `%c✓ Complete`,
              `color: ${this.colors.complete}; font-weight: bold`,
              `(${elapsed.toFixed(2)}ms, ${emissionCount} emissions)`
            );
            
            if (options.groupLogs) {
              console.groupEnd();
            }
            
            subscriber.complete();
          }
        });
        
        return () => {
          if (options.logUnsubscribe) {
            console.log(
              `%c[${name}] Unsubscribe`,
              `color: ${this.colors.unsubscribe}; font-weight: bold`
            );
          }
          subscription.unsubscribe();
        };
      });
    };
  }
}

// Usage
const logger = new ObservableLogger();

const users$ = this.http.get<User[]>('/api/users').pipe(
  logger.log('API Request', {
    groupLogs: true,
    logSubscription: true,
    logUnsubscribe: true,
    tableFormat: true,
    stackTrace: true
  }),
  map(users => users.filter(u => u.active)),
  logger.log('After Filter', { groupLogs: true })
);

users$.subscribe();

Example: Production-safe conditional logging

// Environment-aware logging
const logInDev = <T>(label: string) => {
  return (source: Observable<T>): Observable<T> => {
    // Only log in development
    if (!environment.production) {
      return source.pipe(
        tap({
          next: (val) => console.log(`[${label}] Next:`, val),
          error: (err) => console.error(`[${label}] Error:`, err),
          complete: () => console.log(`[${label}] Complete`)
        })
      );
    }
    
    return source;
  };
};

// Conditional logging based on criteria
const logIf = <T>(
  predicate: (value: T) => boolean,
  label: string
) => {
  return tap<T>(value => {
    if (predicate(value)) {
      console.log(`[${label}]`, value);
    }
  });
};

// Usage
const data$ = getData().pipe(
  logInDev('getData'),
  map(processData),
  logIf(data => data.length === 0, 'Empty Data Warning'),
  logIf(data => data.length > 100, 'Large Dataset Warning')
);

5. Stack Trace Preservation and Error Context

Technique Implementation Benefit Overhead
Error Enhancement Wrap errors with context Additional debugging info Low
Stack Capture new Error().stack at creation Original call site Medium
Error Chain Preserve original error Full error history Low
Source Maps TypeScript/bundler config Original source mapping None (build time)
Async Stack Zone.js integration Full async call stack High

Example: Enhanced error context

// Custom error class with context
class ObservableError extends Error {
  constructor(
    message: string,
    public context: {
      operatorName?: string;
      streamName?: string;
      subscriptionStack?: string;
      timestamp?: number;
      additionalData?: any;
    },
    public originalError?: Error
  ) {
    super(message);
    this.name = 'ObservableError';
    
    // Preserve original stack if available
    if (originalError?.stack) {
      this.stack = `${this.stack}\n\nCaused by:\n${originalError.stack}`;
    }
  }
}

// Operator to add error context
const withErrorContext = <T>(
  streamName: string,
  operatorName: string
) => {
  // Capture subscription stack
  const subscriptionStack = new Error().stack;
  
  return (source: Observable<T>): Observable<T> => {
    return source.pipe(
      catchError(err => {
        const enhancedError = new ObservableError(
          `Error in ${streamName}`,
          {
            operatorName,
            streamName,
            subscriptionStack,
            timestamp: Date.now()
          },
          err
        );
        
        return throwError(() => enhancedError);
      })
    );
  };
};

// Usage
const data$ = this.http.get('/api/data').pipe(
  withErrorContext('data-fetch', 'http.get'),
  map(data => data.value),
  withErrorContext('data-fetch', 'map'),
  filter(value => value !== null),
  withErrorContext('data-fetch', 'filter')
);

data$.subscribe({
  error: (err: ObservableError) => {
    console.error('Stream:', err.context.streamName);
    console.error('Operator:', err.context.operatorName);
    console.error('Subscription Stack:', err.context.subscriptionStack);
    console.error('Original Error:', err.originalError);
  }
});

Example: Zone.js async stack traces (Angular)

// Enable long stack trace in Angular development
// zone-flags.ts (loaded before zone.js)

// Development configuration
if (!environment.production) {
  // Enable long stack trace
  (window as any).__Zone_enable_long_stack_trace = true;
  
  // More detailed error messages
  (window as any).__zone_symbol__UNPATCHED_EVENTS = [];
}

// main.ts
import './zone-flags';
import 'zone.js';

// Now errors will show full async stack
const data$ = this.http.get('/api/data').pipe(
  switchMap(data => this.processData(data)),
  map(result => result.value)
);

// Error will show:
// 1. Where observable was created
// 2. All async boundaries crossed
// 3. Original error location
// 4. Full chain of operators

6. Production Debugging and Monitoring Strategies

Strategy Implementation Use Case Tools
Error Tracking Sentry, Rollbar integration Capture production errors Sentry, Rollbar, Bugsnag
Performance Monitoring Custom metrics, APM tools Track operator performance New Relic, DataDog
Analytics Events Track observable completions User behavior insights Google Analytics, Mixpanel
Health Checks Monitor critical streams Detect stream failures Custom health endpoints
Feature Flags Toggle debugging at runtime Debug specific users LaunchDarkly, Split
Replay Sessions Record user sessions Reproduce issues LogRocket, FullStory

Example: Production error tracking

// Sentry integration for RxJS
import * as Sentry from '@sentry/angular';

// Global error handler for observables
const captureObservableError = <T>(
  context: string
) => {
  return (source: Observable<T>): Observable<T> => {
    return source.pipe(
      catchError(error => {
        // Send to Sentry with context
        Sentry.captureException(error, {
          tags: {
            errorType: 'observable-error',
            context
          },
          extra: {
            timestamp: new Date().toISOString(),
            userAgent: navigator.userAgent
          }
        });
        
        // Re-throw or return fallback
        return throwError(() => error);
      })
    );
  };
};

// Usage in services
@Injectable()
export class DataService {
  getData(): Observable<Data> {
    return this.http.get<Data>('/api/data').pipe(
      captureObservableError('DataService.getData'),
      retry(2),
      catchError(err => {
        // Log additional context
        console.error('Data fetch failed after retries');
        return of({ fallback: true } as Data);
      })
    );
  }
}

Example: Performance monitoring

// Custom performance monitoring
class PerformanceMonitor {
  private metrics = new Map<string, Metric>();
  
  track<T>(operationName: string) {
    return (source: Observable<T>): Observable<T> => {
      return new Observable<T>(subscriber => {
        const startTime = performance.now();
        let emissionCount = 0;
        
        const subscription = source.subscribe({
          next: (value) => {
            emissionCount++;
            subscriber.next(value);
          },
          error: (err) => {
            this.recordMetric(operationName, {
              duration: performance.now() - startTime,
              emissionCount,
              status: 'error'
            });
            subscriber.error(err);
          },
          complete: () => {
            this.recordMetric(operationName, {
              duration: performance.now() - startTime,
              emissionCount,
              status: 'complete'
            });
            subscriber.complete();
          }
        });
        
        return () => subscription.unsubscribe();
      });
    };
  }
  
  private recordMetric(name: string, data: MetricData) {
    // Send to analytics service
    if (window.gtag) {
      window.gtag('event', 'observable_performance', {
        event_category: 'RxJS',
        event_label: name,
        value: Math.round(data.duration),
        custom_dimensions: {
          emissions: data.emissionCount,
          status: data.status
        }
      });
    }
    
    // Store locally for aggregation
    this.metrics.set(name, data);
  }
  
  getMetrics(): Map<string, Metric> {
    return this.metrics;
  }
  
  // Send metrics batch to server
  flush(): Observable<void> {
    const metrics = Array.from(this.metrics.entries());
    
    return this.http.post('/api/metrics', { metrics }).pipe(
      tap(() => this.metrics.clear()),
      map(() => undefined)
    );
  }
}

// Usage
const monitor = new PerformanceMonitor();

const data$ = this.http.get('/api/data').pipe(
  monitor.track('api-fetch'),
  map(transform),
  monitor.track('data-transform')
);

// Periodically flush metrics
interval(60000).pipe(
  switchMap(() => monitor.flush())
).subscribe();

Example: Feature-flag controlled debugging

// Dynamic debug toggle with feature flags
class DebugService {
  private debugEnabled$ = new BehaviorSubject<boolean>(false);
  
  constructor(
    private featureFlags: FeatureFlagService
  ) {
    // Check feature flag
    this.featureFlags.isEnabled('rxjs-debug').subscribe(enabled => {
      this.debugEnabled$.next(enabled);
    });
  }
  
  // Conditional debug operator
  debug<T>(label: string) {
    return (source: Observable<T>): Observable<T> => {
      return this.debugEnabled$.pipe(
        take(1),
        switchMap(enabled => {
          if (enabled) {
            return source.pipe(
              tap({
                next: val => console.log(`[${label}]`, val),
                error: err => console.error(`[${label}] Error:`, err),
                complete: () => console.log(`[${label}] Complete`)
              })
            );
          }
          return source;
        })
      );
    };
  }
  
  // Enable debug for specific user
  enableForUser(userId: string) {
    const storedDebugUsers = localStorage.getItem('debug-users') || '[]';
    const debugUsers = JSON.parse(storedDebugUsers);
    
    if (!debugUsers.includes(userId)) {
      debugUsers.push(userId);
      localStorage.setItem('debug-users', JSON.stringify(debugUsers));
      this.debugEnabled$.next(true);
    }
  }
}

// Usage
@Injectable()
export class UserService {
  constructor(private debug: DebugService) {}
  
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      this.debug.debug('fetch-users'),
      map(users => users.filter(u => u.active)),
      this.debug.debug('filter-users')
    );
  }
}

// Enable debugging from console
// window.enableRxjsDebug('user-123');
Production Debugging Warnings:
  • ⚠️ Remove or disable DevTools instrumentation in production builds
  • ⚠️ Use conditional logging to avoid console spam
  • ⚠️ Implement error tracking to capture production issues
  • ⚠️ Monitor performance metrics to detect slow operators
  • ⚠️ Consider privacy when logging user data
Debugging Best Practices:
  • Development: Use DevTools, verbose logging, stack traces
  • Staging: Enable selective debugging, performance monitoring
  • Production: Error tracking, health checks, feature-flag controlled logs
  • Testing: Use marble diagrams, TestScheduler for deterministic tests
  • Monitoring: Track subscription counts, emission rates, error rates

Section 18 Summary

  • DevTools - browser extensions provide visual debugging, timeline views, subscription tracking
  • tap operator - essential for non-intrusive stream debugging and side-effect logging
  • Stream visualization - tools like RxViz and custom inspectors reveal observable behavior
  • Console patterns - structured logging with colors, groups, tables improves clarity
  • Stack traces - preserve error context and async call stacks for better debugging
  • Production monitoring - integrate Sentry, performance tracking, feature flags for live debugging