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