ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
216 lines • 7.31 kB
JavaScript
import { nanoid } from 'nanoid';
export class DebugSessionRecorder {
sessionId;
events = [];
_startTime = 0;
_isRecording = false;
page;
options;
eventBuffer = [];
maxBufferSize;
sessionMetadata = null;
constructor(page, options = {}) {
this.sessionId = nanoid();
this.page = page;
this.options = options;
this.maxBufferSize = options.bufferSize || 1000;
}
get isRecording() {
return this._isRecording;
}
get startTime() {
return this._startTime;
}
async startRecording(metadata) {
if (this._isRecording) {
throw new Error('Recording already in progress');
}
this._isRecording = true;
this._startTime = Date.now();
this.events = [];
this.eventBuffer = [];
this.sessionMetadata = metadata;
// Attach event listeners
this.attachBrowserListeners();
this.attachNetworkListeners();
this.attachConsoleListeners();
this.attachErrorListeners();
// Initial state capture
await this.captureInitialState();
}
attachBrowserListeners() {
// Navigation events (valid Playwright event)
this.page.on('framenavigated', (frame) => {
if (frame === this.page.mainFrame()) {
this.recordEvent({
type: 'navigation',
timestamp: Date.now() - this._startTime,
data: {
url: this.filterUrl(frame.url()),
method: 'navigation'
}
});
}
});
// Page close event (valid Playwright event)
this.page.on('close', () => {
this._isRecording = false;
});
// For click and input events, we'd need to inject client-side listeners
// This is a simplified implementation for testing purposes
// In a real implementation, we'd use page.evaluate() to inject listeners
}
attachNetworkListeners() {
this.page.on('request', (request) => {
this.recordEvent({
type: 'network_request',
timestamp: Date.now() - this._startTime,
data: {
url: this.filterUrl(request.url()),
method: request.method(),
headers: this.filterHeaders(request.headers()),
requestBody: this.filterBody(request.postData())
}
});
});
this.page.on('response', (response) => {
this.recordEvent({
type: 'network_response',
timestamp: Date.now() - this._startTime,
data: {
url: this.filterUrl(response.url()),
status: response.status(),
headers: this.filterHeaders(response.headers())
}
});
});
}
attachConsoleListeners() {
this.page.on('console', (msg) => {
this.recordEvent({
type: 'console_log',
timestamp: Date.now() - this._startTime,
data: {
level: msg.type(),
text: msg.text(),
location: msg.location()
}
});
});
}
attachErrorListeners() {
this.page.on('pageerror', (error) => {
this.recordEvent({
type: 'error',
timestamp: Date.now() - this._startTime,
data: {
error: error.message,
stack: error.stack
}
});
});
}
async captureInitialState() {
try {
const viewport = await this.page.viewportSize();
this.recordEvent({
type: 'navigation',
timestamp: 0,
data: {
url: this.page.url(),
method: 'initial',
viewport: viewport || { width: 1920, height: 1080 }
}
});
}
catch (error) {
// Silently ignore initial state capture errors
}
}
async getElementInfo(event) {
// Mock implementation for testing - in real implementation this would
// extract element information from the event
return {
selector: event.target?.id ? `#${event.target.id}` : event.target?.tagName?.toLowerCase() || 'unknown',
tag: event.target?.tagName || 'UNKNOWN',
text: event.target?.textContent || '',
attributes: event.target?.attributes || {},
inputType: event.target?.type || ''
};
}
recordEvent(event) {
// Add to buffer with size limit
this.eventBuffer.push(event);
// Maintain buffer size limit
if (this.eventBuffer.length > this.maxBufferSize) {
this.eventBuffer = this.eventBuffer.slice(-this.maxBufferSize);
}
}
getEvents() {
return [...this.eventBuffer];
}
filterValue(value, fieldType) {
if (!this.options.privacy?.redactSensitiveData) {
return value;
}
const sensitiveFields = this.options.privacy.sensitiveFields || [
'password', 'passwd', 'pwd', 'secret', 'token', 'key', 'ssn', 'social'
];
if (sensitiveFields.some(field => fieldType.toLowerCase().includes(field))) {
return '[REDACTED]';
}
return value;
}
filterUrl(url) {
if (!this.options.privacy?.excludedDomains) {
return url;
}
try {
const urlObj = new URL(url);
const domain = urlObj.hostname;
if (this.options.privacy.excludedDomains.includes(domain)) {
return `[FILTERED_DOMAIN]${urlObj.pathname}${urlObj.search}`;
}
}
catch (error) {
// Return original URL if parsing fails
}
return url;
}
filterHeaders(headers) {
const filtered = { ...headers };
// Remove sensitive headers
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'];
sensitiveHeaders.forEach(header => {
if (filtered[header]) {
filtered[header] = '[REDACTED]';
}
});
return filtered;
}
filterBody(body) {
if (!body)
return null;
// In a real implementation, this would parse and filter sensitive data
// For now, just return the body
return body;
}
async stopRecording() {
if (!this._isRecording) {
throw new Error('No recording in progress');
}
this._isRecording = false;
const endTime = Date.now();
const duration = endTime - this._startTime;
const session = {
id: this.sessionId,
timestamp: new Date(this._startTime),
duration,
url: this.page.url(),
events: [...this.eventBuffer],
metadata: this.sessionMetadata || { testIntent: 'Unknown' }
};
return session;
}
}
//# sourceMappingURL=debug-session-recorder.js.map