/**
* BreakpointManager - Centralized breakpoint management and analytics
*
* Handles:
* - Breakpoint state management and persistence
* - Analytics and hit tracking
* - Coordination between Chrome DevTools and MCP interface
* - Conditional breakpoint evaluation
* - Logpoint execution and formatting
*/
import { EventEmitter } from "events";
import { ConfigManager } from "../config/ConfigManager.js";
import { ChromeDebugger, BreakpointLocation, BreakpointOptions, BreakpointInfo, BreakpointHit } from "../debuggers/ChromeDebugger.js";
import { Logger } from "../utils/Logger.js";
export interface BreakpointAnalytics {
totalBreakpoints: number;
activeBreakpoints: number;
totalHits: number;
averageHitsPerBreakpoint: number;
mostHitBreakpoint?: BreakpointInfo;
recentHits: BreakpointHit[];
hitsByFile: Record<string, number>;
hitsByHour: Record<string, number>;
}
export interface BreakpointManagerConfig {
maxRecentHits: number;
autoRemoveAfterHits?: number;
enableAnalytics: boolean;
persistBreakpoints: boolean;
logpointTimeout: number;
}
export interface BreakpointCommand {
action: "set" | "remove" | "list" | "clear" | "toggle" | "analytics";
location?: BreakpointLocation;
options?: BreakpointOptions;
breakpointId?: string;
active?: boolean;
}
export interface BreakpointResult {
success: boolean;
message: string;
data?: any;
breakpoint?: BreakpointInfo;
breakpoints?: BreakpointInfo[];
analytics?: BreakpointAnalytics;
}
export class BreakpointManager extends EventEmitter {
private logger: Logger;
private configManager: ConfigManager;
private chromeDebugger: ChromeDebugger;
private recentHits: BreakpointHit[] = [];
private hitsByFile: Map<string, number> = new Map();
private hitsByHour: Map<string, number> = new Map();
private config: BreakpointManagerConfig;
constructor(configManager: ConfigManager, chromeDebugger: ChromeDebugger) {
super();
this.logger = new Logger("BreakpointManager");
this.configManager = configManager;
this.chromeDebugger = chromeDebugger;
// Load configuration
this.config = this.loadConfig();
// Set up event listeners
this.setupEventListeners();
}
/**
* Initialize the breakpoint manager
*/
async initialize(): Promise<void> {
try {
this.logger.info("Initializing BreakpointManager...");
// Load persisted breakpoints if enabled
if (this.config.persistBreakpoints) {
await this.loadPersistedBreakpoints();
}
this.logger.info("BreakpointManager initialized successfully");
} catch (error) {
this.logger.error("Failed to initialize BreakpointManager:", error);
throw error;
}
}
/**
* Load configuration from ConfigManager
*/
private loadConfig(): BreakpointManagerConfig {
const globalConfig = this.configManager.getConfig();
return {
maxRecentHits: globalConfig.breakpoints?.maxRecentHits || 100,
autoRemoveAfterHits: globalConfig.breakpoints?.autoRemoveAfterHits,
enableAnalytics: globalConfig.breakpoints?.enableAnalytics !== false,
persistBreakpoints: globalConfig.breakpoints?.persistBreakpoints !== false,
logpointTimeout: globalConfig.breakpoints?.logpointTimeout || 5000
};
}
/**
* Set up event listeners for Chrome debugger events
*/
private setupEventListeners(): void {
// Listen for breakpoint hits
this.chromeDebugger.on('breakpointHit', (hit: BreakpointHit) => {
this.handleBreakpointHit(hit);
});
// Listen for breakpoint lifecycle events
this.chromeDebugger.on('breakpointSet', (breakpoint: BreakpointInfo) => {
this.emit('breakpointSet', breakpoint);
this.logger.info(`Breakpoint set: ${breakpoint.id}`);
});
this.chromeDebugger.on('breakpointRemoved', (breakpoint: BreakpointInfo) => {
this.emit('breakpointRemoved', breakpoint);
this.logger.info(`Breakpoint removed: ${breakpoint.id}`);
});
// Listen for configuration changes
this.configManager.on('configUpdated', () => {
this.config = this.loadConfig();
this.logger.info("BreakpointManager configuration updated");
});
}
/**
* Handle breakpoint hit events and update analytics
*/
private handleBreakpointHit(hit: BreakpointHit): void {
if (!this.config.enableAnalytics) return;
// Add to recent hits
this.recentHits.unshift(hit);
if (this.recentHits.length > this.config.maxRecentHits) {
this.recentHits = this.recentHits.slice(0, this.config.maxRecentHits);
}
// Update file-based statistics
const breakpoint = this.chromeDebugger.getBreakpoint(hit.breakpointId);
if (breakpoint) {
const filePath = breakpoint.location.filePath || breakpoint.location.url || 'unknown';
const currentCount = this.hitsByFile.get(filePath) || 0;
this.hitsByFile.set(filePath, currentCount + 1);
// Update hourly statistics
const hour = new Date().toISOString().slice(0, 13); // YYYY-MM-DDTHH
const hourlyCount = this.hitsByHour.get(hour) || 0;
this.hitsByHour.set(hour, hourlyCount + 1);
// Check for auto-removal
if (this.config.autoRemoveAfterHits && breakpoint.hitCount >= this.config.autoRemoveAfterHits) {
this.removeBreakpoint(hit.breakpointId).catch(error => {
this.logger.error(`Failed to auto-remove breakpoint ${hit.breakpointId}:`, error);
});
}
}
this.emit('breakpointHit', hit);
}
/**
* Execute a breakpoint command
*/
async executeCommand(command: BreakpointCommand): Promise<BreakpointResult> {
try {
switch (command.action) {
case "set":
return await this.setBreakpoint(command.location!, command.options);
case "remove":
return await this.removeBreakpoint(command.breakpointId!);
case "list":
return this.listBreakpoints();
case "clear":
return await this.clearAllBreakpoints();
case "toggle":
return await this.toggleBreakpoints(command.active!);
case "analytics":
return this.getAnalytics();
default:
return {
success: false,
message: `Unknown breakpoint action: ${command.action}`
};
}
} catch (error) {
this.logger.error(`Failed to execute breakpoint command ${command.action}:`, error);
return {
success: false,
message: `Failed to execute ${command.action}: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Set a new breakpoint
*/
async setBreakpoint(location: BreakpointLocation, options: BreakpointOptions = {}): Promise<BreakpointResult> {
if (!this.chromeDebugger.isDebuggerEnabled()) {
return {
success: false,
message: "Chrome debugger not connected or enabled"
};
}
try {
const breakpoint = await this.chromeDebugger.setBreakpoint(location, options);
return {
success: true,
message: `Breakpoint set at ${location.filePath || location.url}:${location.lineNumber}`,
breakpoint
};
} catch (error) {
return {
success: false,
message: `Failed to set breakpoint: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Remove a breakpoint
*/
async removeBreakpoint(breakpointId: string): Promise<BreakpointResult> {
if (!this.chromeDebugger.isDebuggerEnabled()) {
return {
success: false,
message: "Chrome debugger not connected or enabled"
};
}
try {
const removed = await this.chromeDebugger.removeBreakpoint(breakpointId);
if (removed) {
return {
success: true,
message: `Breakpoint ${breakpointId} removed successfully`
};
} else {
return {
success: false,
message: `Breakpoint ${breakpointId} not found`
};
}
} catch (error) {
return {
success: false,
message: `Failed to remove breakpoint: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* List all breakpoints
*/
listBreakpoints(): BreakpointResult {
const breakpoints = this.chromeDebugger.getBreakpoints();
return {
success: true,
message: `Found ${breakpoints.length} breakpoint(s)`,
breakpoints
};
}
/**
* Clear all breakpoints
*/
async clearAllBreakpoints(): Promise<BreakpointResult> {
if (!this.chromeDebugger.isDebuggerEnabled()) {
return {
success: false,
message: "Chrome debugger not connected or enabled"
};
}
try {
await this.chromeDebugger.clearAllBreakpoints();
return {
success: true,
message: "All breakpoints cleared successfully"
};
} catch (error) {
return {
success: false,
message: `Failed to clear breakpoints: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Toggle breakpoints active/inactive
*/
async toggleBreakpoints(active: boolean): Promise<BreakpointResult> {
if (!this.chromeDebugger.isDebuggerEnabled()) {
return {
success: false,
message: "Chrome debugger not connected or enabled"
};
}
try {
await this.chromeDebugger.setBreakpointsActive(active);
return {
success: true,
message: `Breakpoints ${active ? 'enabled' : 'disabled'} successfully`
};
} catch (error) {
return {
success: false,
message: `Failed to toggle breakpoints: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Get breakpoint analytics
*/
getAnalytics(): BreakpointResult {
if (!this.config.enableAnalytics) {
return {
success: false,
message: "Analytics disabled in configuration"
};
}
const breakpoints = this.chromeDebugger.getBreakpoints();
const totalHits = breakpoints.reduce((sum, bp) => sum + bp.hitCount, 0);
const activeBreakpoints = breakpoints.filter(bp => bp.enabled).length;
// Find most hit breakpoint
const mostHitBreakpoint = breakpoints.reduce((max, bp) =>
bp.hitCount > (max?.hitCount || 0) ? bp : max, undefined as BreakpointInfo | undefined);
const analytics: BreakpointAnalytics = {
totalBreakpoints: breakpoints.length,
activeBreakpoints,
totalHits,
averageHitsPerBreakpoint: breakpoints.length > 0 ? totalHits / breakpoints.length : 0,
mostHitBreakpoint,
recentHits: this.recentHits.slice(0, 10), // Last 10 hits
hitsByFile: Object.fromEntries(this.hitsByFile),
hitsByHour: Object.fromEntries(this.hitsByHour)
};
return {
success: true,
message: "Analytics retrieved successfully",
analytics
};
}
/**
* Load persisted breakpoints (placeholder for future implementation)
*/
private async loadPersistedBreakpoints(): Promise<void> {
// TODO: Implement breakpoint persistence
this.logger.info("Breakpoint persistence not yet implemented");
}
/**
* Shutdown the breakpoint manager
*/
async shutdown(): Promise<void> {
try {
this.logger.info("Shutting down BreakpointManager...");
// Clear analytics data
this.recentHits = [];
this.hitsByFile.clear();
this.hitsByHour.clear();
this.logger.info("BreakpointManager shutdown complete");
} catch (error) {
this.logger.error("Error during BreakpointManager shutdown:", error);
throw error;
}
}
}