// HTTP request classification helpers -- kept separate from metrics.js // Separated from server.js to keep HTTP method classification in its own module. /** * Derive a short action name from an Express request for metrics labeling. */ export function actionFromReq(req) { const method = req.method; const routePath = req.route?.path; const path = routePath || req.path || ''; if (path === '/tabs' && method === 'POST') return 'create_tab'; if (path === '/tabs/:tabId' && method === 'DELETE') return 'delete_tab'; if (path === '/tabs/group/:listItemId' && method === 'DELETE') return 'delete_tab_group'; if (path === '/sessions/:userId' && method === 'DELETE') return 'delete_session'; if (path === '/sessions/:userId/cookies' && method === 'POST') return 'set_cookies'; if (path === '/tabs/open' && method === 'POST') return 'open_url'; if (path === '/tabs' && method === 'GET') return 'list_tabs'; const tabAction = path.match(/^\/tabs\/(?:[^/]+|:tabId)\/(\w+)$/); if (tabAction) return tabAction[1]; if (path.match(/^\/tabs\/(?:[^/]+|:tabId)$/) && method === 'DELETE') return 'delete_tab'; if (path.match(/^\/tabs\/group\/(?:[^/]+|:listItemId)$/) && method === 'DELETE') return 'delete_tab_group'; if (path.match(/^\/sessions\/(?:[^/]+|:userId)$/) && method === 'DELETE') return 'delete_session'; if (path.match(/^\/sessions\/(?:[^/]+|:userId)\/cookies$/) && method === 'POST') return 'set_cookies'; // legacy compat routes if (['/start', '/stop', '/navigate', '/snapshot', '/act'].includes(path)) return path.slice(1); if (path === '/youtube/transcript') return 'youtube_transcript'; if (path === '/health') return 'health'; if (path === '/metrics') return 'metrics'; return `${method.toLowerCase()}_${path.replace(/[/:]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}`; } /** * Classify an error into a failure type string for metrics labeling. */ export function classifyError(err) { if (!err) return 'unknown'; const msg = err.message || ''; if (err.code === 'stale_refs' || err.name === 'StaleRefsError') return 'stale_refs'; if (err.code === 'element_not_actionable') return 'element_error'; if (err.code === 'invalid_selector' || (msg.includes('Unexpected token') && msg.includes('selector')) || (msg.includes('Unsupported token') && msg.includes('selector')) || msg.includes('Malformed selector') || msg.includes('Invalid selector') || msg.includes('is not a valid selector')) return 'invalid_selector'; if (msg === 'Tab lock queue timeout') return 'tab_lock_timeout'; if (msg === 'Tab destroyed') return 'tab_destroyed'; if (msg.includes('Target page, context or browser has been closed') || msg.includes('browser has been closed') || msg.includes('Context closed') || msg.includes('Browser closed')) return 'dead_context'; if (msg.includes('timed out after') || (msg.includes('Timeout') && msg.includes('exceeded'))) return 'timeout'; if (msg.includes('Maximum concurrent sessions')) return 'session_limit'; if (msg.includes('Maximum tabs per session') || msg.includes('Maximum global tabs')) return 'tab_limit'; if (msg.includes('User concurrency limit reached') || msg.includes('concurrency limit reached')) return 'concurrency_timeout'; if (msg.includes('NS_ERROR_PROXY') || msg.includes('proxy connection') || msg.includes('Proxy connection')) return 'proxy'; if (msg.includes('Browser launch timeout') || msg.includes('Failed to launch')) return 'browser_launch'; if (msg.includes('strict mode violation') && msg.includes('resolved to')) return 'ambiguous_selector'; if (msg.includes('intercepts pointer events')) return 'element_error'; if (msg.includes('not visible') || msg.includes('Malformed value') || msg.includes('not an ') || msg.includes('Input of type "submit" cannot be filled') || msg.includes('is not fillable') || msg.includes('element is not attached') || msg.includes('Element is not attached') || msg.includes('no bounding box')) return 'element_error'; if (msg.includes('Blocked URL scheme') || msg.includes('Invalid URL')) return 'invalid_url'; if (msg.includes('Navigation aborted: tab deleted')) return 'tab_destroyed'; if (msg.includes('Execution context was destroyed') || msg.includes('Cannot find context with specified id') || msg.includes('Frame was detached') || msg.includes('Navigation interrupted') || msg.includes('NS_ERROR_NET_INTERRUPT') || msg.includes('NS_BINDING_ABORTED')) return 'navigation_race'; if (msg.includes('NS_ERROR_ABORT')) return 'nav_aborted'; if (msg.includes('Page crashed') || msg.includes('Target crashed') || msg.includes('crashed page')) return 'page_crashed'; if (msg.includes('Navigation failed') || msg.includes('ERR_ABORTED')) return 'nav_aborted'; if (msg.includes('net::') || msg.includes('ERR_NAME') || msg.includes('ERR_CONNECTION')) return 'network'; return 'unknown'; }