UNPKG

@probelabs/probe-chat

Version:

CLI and web interface for Probe code search (formerly @probelabs/probe-web and @probelabs/probe-chat)

947 lines (827 loc) 34.6 kB
/** * Custom Application Tracing Layer for Probe Chat * * This module provides granular tracing that follows application logic closely, * replacing the generic Vercel AI SDK tracing with application-specific spans. */ import { trace, SpanStatusCode, SpanKind, context, TraceFlags } from '@opentelemetry/api'; import { randomUUID, createHash } from 'crypto'; /** * Convert a session ID to a valid OpenTelemetry trace ID (32-char hex) */ function sessionIdToTraceId(sessionId) { // Create a hash of the session ID and take first 32 chars const hash = createHash('sha256').update(sessionId).digest('hex'); return hash.substring(0, 32); } // OpenTelemetry semantic conventions and custom attributes const OTEL_ATTRS = { // Standard semantic conventions SERVICE_NAME: 'service.name', SERVICE_VERSION: 'service.version', HTTP_METHOD: 'http.method', HTTP_STATUS_CODE: 'http.status_code', ERROR_TYPE: 'error.type', ERROR_MESSAGE: 'error.message', // Custom application attributes following OpenTelemetry naming conventions APP_SESSION_ID: 'app.session.id', APP_MESSAGE_TYPE: 'app.message.type', APP_MESSAGE_CONTENT: 'app.message.content', APP_MESSAGE_LENGTH: 'app.message.length', APP_MESSAGE_HASH: 'app.message.hash', APP_AI_PROVIDER: 'app.ai.provider', APP_AI_MODEL: 'app.ai.model', APP_AI_TEMPERATURE: 'app.ai.temperature', APP_AI_MAX_TOKENS: 'app.ai.max_tokens', APP_AI_RESPONSE_CONTENT: 'app.ai.response.content', APP_AI_RESPONSE_LENGTH: 'app.ai.response.length', APP_AI_RESPONSE_HASH: 'app.ai.response.hash', APP_AI_COMPLETION_TOKENS: 'app.ai.completion_tokens', APP_AI_PROMPT_TOKENS: 'app.ai.prompt_tokens', APP_AI_FINISH_REASON: 'app.ai.finish_reason', APP_TOOL_NAME: 'app.tool.name', APP_TOOL_PARAMS: 'app.tool.params', APP_TOOL_RESULT: 'app.tool.result', APP_TOOL_SUCCESS: 'app.tool.success', APP_ITERATION_NUMBER: 'app.iteration.number' }; class AppTracer { constructor() { // Use consistent tracer name across the application this.tracer = trace.getTracer('probe-chat', '1.0.0'); this.activeSpans = new Map(); this.sessionSpans = new Map(); this.sessionContexts = new Map(); // Store active context for each session } /** * Get the shared tracer instance */ getTracer() { return this.tracer; } /** * Hash a string for deduplication purposes */ _hashString(str) { let hash = 0; if (str.length === 0) return hash; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash.toString(); } /** * Get the active context for a session, creating spans within the session trace */ _getSessionContext(sessionId) { return this.sessionContexts.get(sessionId) || context.active(); } /** * Start a chat session span with custom trace ID based on session ID */ startChatSession(sessionId, userMessage, provider, model) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Starting chat session span for ${sessionId}`); } // Create a custom trace ID from the session ID const traceId = sessionIdToTraceId(sessionId); // Generate a span ID for the root span const spanId = randomUUID().replace(/-/g, '').substring(0, 16); // Create trace context with custom trace ID const spanContext = { traceId: traceId, spanId: spanId, traceFlags: TraceFlags.SAMPLED, isRemote: false }; // Create a new context with our custom trace context const activeContext = trace.setSpanContext(context.active(), spanContext); // Start the span within this custom context const span = context.with(activeContext, () => { return this.tracer.startSpan('messaging.process', { kind: SpanKind.SERVER, attributes: { [OTEL_ATTRS.APP_SESSION_ID]: sessionId, [OTEL_ATTRS.APP_MESSAGE_CONTENT]: userMessage.substring(0, 500), // Capture more message content [OTEL_ATTRS.APP_MESSAGE_LENGTH]: userMessage.length, [OTEL_ATTRS.APP_MESSAGE_HASH]: this._hashString(userMessage), // Add hash for deduplication [OTEL_ATTRS.APP_AI_PROVIDER]: provider, [OTEL_ATTRS.APP_AI_MODEL]: model, 'app.session.start_time': Date.now(), 'app.trace.custom_id': true // Mark that we're using custom trace ID } }); }); if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Created chat session span ${span.spanContext().spanId} in trace ${span.spanContext().traceId}`); } // Create session context with the span as the active span const sessionContext = trace.setSpan(context.active(), span); this.sessionContexts.set(sessionId, sessionContext); this.sessionSpans.set(sessionId, span); if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Session context established for ${sessionId}`); } return span; } /** * Execute a function within the session context to ensure proper trace correlation */ withSessionContext(sessionId, fn) { const sessionContext = this._getSessionContext(sessionId); return context.with(sessionContext, fn); } /** * Get the trace ID for a session (derived from session ID) */ getTraceIdForSession(sessionId) { return sessionIdToTraceId(sessionId); } /** * Start processing a user message */ startUserMessageProcessing(sessionId, messageId, message, imageUrlsFound = 0) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Starting user message processing span for ${sessionId}`); } const sessionContext = this._getSessionContext(sessionId); return context.with(sessionContext, () => { // Get the parent span (should be the session span) from the context const parentSpan = trace.getActiveSpan(); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { [OTEL_ATTRS.APP_SESSION_ID]: sessionId, 'app.message.id': messageId, [OTEL_ATTRS.APP_MESSAGE_TYPE]: 'user', [OTEL_ATTRS.APP_MESSAGE_CONTENT]: message.substring(0, 1000), // Include actual message content [OTEL_ATTRS.APP_MESSAGE_LENGTH]: message.length, [OTEL_ATTRS.APP_MESSAGE_HASH]: this._hashString(message), 'app.message.image_urls_found': imageUrlsFound, 'app.processing.start_time': Date.now() } }; // Explicitly set the parent if available if (parentSpan) { spanOptions.parent = parentSpan.spanContext(); } const span = this.tracer.startSpan('messaging.message.process', spanOptions); if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Created user message processing span ${span.spanContext().spanId} with parent ${parentSpan?.spanContext().spanId}`); } this.activeSpans.set(`${sessionId}_user_processing`, span); // DO NOT overwrite the session context - this breaks parent-child relationships // Instead, create a temporary context for this message processing without storing it const messageContext = trace.setSpan(sessionContext, span); // Store the message context temporarily for child operations, but keep session context intact this.sessionContexts.set(`${sessionId}_message_processing`, messageContext); return span; }); } /** * Execute a function within the context of user message processing span */ withUserProcessingContext(sessionId, fn) { const span = this.activeSpans.get(`${sessionId}_user_processing`); if (span) { return context.with(trace.setSpan(context.active(), span), fn); } return fn(); } /** * Start the agent loop */ startAgentLoop(sessionId, maxIterations) { const sessionContext = this._getSessionContext(sessionId); return context.with(sessionContext, () => { // Get the parent span from the context const parentSpan = trace.getActiveSpan(); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.loop.max_iterations': maxIterations, 'app.loop.start_time': Date.now() } }; // Explicitly set the parent if available if (parentSpan) { spanOptions.parent = parentSpan.spanContext(); } const span = this.tracer.startSpan('agent.loop.start', spanOptions); this.activeSpans.set(`${sessionId}_agent_loop`, span); // DO NOT overwrite the session context - store agent loop context separately const agentLoopContext = trace.setSpan(sessionContext, span); this.sessionContexts.set(`${sessionId}_agent_loop`, agentLoopContext); return span; }); } /** * Execute a function within the context of agent loop span */ withAgentLoopContext(sessionId, fn) { const span = this.activeSpans.get(`${sessionId}_agent_loop`); if (span) { return context.with(trace.setSpan(context.active(), span), fn); } return fn(); } /** * Start a single iteration of the agent loop */ startAgentIteration(sessionId, iterationNumber, messagesCount, contextTokens) { const sessionContext = this._getSessionContext(sessionId); return context.with(sessionContext, () => { const span = this.tracer.startSpan('agent.loop.iteration', { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.iteration.number': iterationNumber, 'app.iteration.messages_count': messagesCount, 'app.iteration.context_tokens': contextTokens, 'app.iteration.start_time': Date.now() } }); this.activeSpans.set(`${sessionId}_iteration_${iterationNumber}`, span); // DO NOT overwrite the session context - store iteration context separately const iterationContext = trace.setSpan(sessionContext, span); this.sessionContexts.set(`${sessionId}_iteration_${iterationNumber}`, iterationContext); return span; }); } /** * Execute a function within the context of agent iteration span */ withIterationContext(sessionId, iterationNumber, fn) { const span = this.activeSpans.get(`${sessionId}_iteration_${iterationNumber}`); if (span) { return context.with(trace.setSpan(context.active(), span), fn); } return fn(); } /** * Start an AI generation request */ startAiGenerationRequest(sessionId, iterationNumber, model, provider, settings = {}, messagesContext = []) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Starting AI generation request span for session ${sessionId}, iteration ${iterationNumber}`); } // Get the most appropriate context - prefer iteration context over session context const iterationContext = this.sessionContexts.get(`${sessionId}_iteration_${iterationNumber}`); const sessionContext = iterationContext || this._getSessionContext(sessionId); return context.with(sessionContext, () => { const span = this.tracer.startSpan('ai.generation.request', { kind: SpanKind.CLIENT, attributes: { [OTEL_ATTRS.APP_SESSION_ID]: sessionId, [OTEL_ATTRS.APP_ITERATION_NUMBER]: iterationNumber, [OTEL_ATTRS.APP_AI_MODEL]: model, [OTEL_ATTRS.APP_AI_PROVIDER]: provider, [OTEL_ATTRS.APP_AI_TEMPERATURE]: settings.temperature || 0, [OTEL_ATTRS.APP_AI_MAX_TOKENS]: settings.maxTokens || 0, 'app.ai.max_retries': settings.maxRetries || 0, 'app.ai.messages_count': messagesContext.length, 'app.ai.request_start_time': Date.now() } }); if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Created AI generation span ${span.spanContext().spanId}`); } this.activeSpans.set(`${sessionId}_ai_request_${iterationNumber}`, span); // Store AI request context separately, don't overwrite session context const aiRequestContext = trace.setSpan(sessionContext, span); this.sessionContexts.set(`${sessionId}_ai_request_${iterationNumber}`, aiRequestContext); return span; }); } /** * Record AI response received */ recordAiResponse(sessionId, iterationNumber, responseData) { const sessionContext = this._getSessionContext(sessionId); return context.with(sessionContext, () => { const span = this.tracer.startSpan('ai.generation.response', { kind: SpanKind.INTERNAL, attributes: { [OTEL_ATTRS.APP_SESSION_ID]: sessionId, [OTEL_ATTRS.APP_ITERATION_NUMBER]: iterationNumber, [OTEL_ATTRS.APP_AI_RESPONSE_CONTENT]: responseData.response ? responseData.response.substring(0, 2000) : '', // Include actual response content [OTEL_ATTRS.APP_AI_RESPONSE_LENGTH]: responseData.responseLength || (responseData.response ? responseData.response.length : 0), [OTEL_ATTRS.APP_AI_RESPONSE_HASH]: responseData.response ? this._hashString(responseData.response) : '', [OTEL_ATTRS.APP_AI_COMPLETION_TOKENS]: responseData.completionTokens || 0, [OTEL_ATTRS.APP_AI_PROMPT_TOKENS]: responseData.promptTokens || 0, [OTEL_ATTRS.APP_AI_FINISH_REASON]: responseData.finishReason || 'unknown', 'app.ai.response.time_to_first_chunk_ms': responseData.timeToFirstChunk || 0, 'app.ai.response.time_to_finish_ms': responseData.timeToFinish || 0, 'app.ai.response.received_time': Date.now() } }); // End the span immediately since this is just recording the response span.setStatus({ code: SpanStatusCode.OK }); span.end(); return span; }); } /** * Record a parsed tool call */ recordToolCallParsed(sessionId, iterationNumber, toolName, toolParams) { const aiRequestSpan = this.activeSpans.get(`${sessionId}_ai_request_${iterationNumber}`); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.tool.name': toolName, 'app.tool.params': JSON.stringify(toolParams).substring(0, 500), // Truncate large params 'app.tool.parsed_time': Date.now() } }; if (aiRequestSpan) { spanOptions.parent = aiRequestSpan.spanContext(); } const span = this.tracer.startSpan('tool.call.parse', spanOptions); // End immediately since this is just recording the parsing span.setStatus({ code: SpanStatusCode.OK }); span.end(); return span; } /** * Start tool execution */ startToolExecution(sessionId, iterationNumber, toolName, toolParams) { // Get the most appropriate context - prefer AI request context over session context const aiRequestContext = this.sessionContexts.get(`${sessionId}_ai_request_${iterationNumber}`); const sessionContext = aiRequestContext || this._getSessionContext(sessionId); return context.with(sessionContext, () => { const span = this.tracer.startSpan('tool.call', { kind: SpanKind.INTERNAL, attributes: { [OTEL_ATTRS.APP_SESSION_ID]: sessionId, [OTEL_ATTRS.APP_ITERATION_NUMBER]: iterationNumber, [OTEL_ATTRS.APP_TOOL_NAME]: toolName, [OTEL_ATTRS.APP_TOOL_PARAMS]: JSON.stringify(toolParams).substring(0, 1000), // Include actual tool parameters 'app.tool.params.hash': this._hashString(JSON.stringify(toolParams)), 'app.tool.execution_start_time': Date.now(), // Add specific attributes based on tool type ...(toolName === 'search' && toolParams.query ? { 'app.tool.search.query': toolParams.query } : {}), ...(toolName === 'extract' && toolParams.file_path ? { 'app.tool.extract.file_path': toolParams.file_path } : {}), ...(toolName === 'query' && toolParams.pattern ? { 'app.tool.query.pattern': toolParams.pattern } : {}), } }); this.activeSpans.set(`${sessionId}_tool_execution_${iterationNumber}`, span); // Store tool execution context separately, don't overwrite session context const toolExecutionContext = trace.setSpan(sessionContext, span); this.sessionContexts.set(`${sessionId}_tool_execution_${iterationNumber}`, toolExecutionContext); return span; }); } /** * End tool execution with results */ endToolExecution(sessionId, iterationNumber, success, resultLength = 0, errorMessage = null, result = null) { const span = this.activeSpans.get(`${sessionId}_tool_execution_${iterationNumber}`); if (!span) return; const attributes = { [OTEL_ATTRS.APP_TOOL_SUCCESS]: success, 'app.tool.result_length': resultLength, 'app.tool.execution_end_time': Date.now(), ...(errorMessage ? { [OTEL_ATTRS.ERROR_MESSAGE]: errorMessage } : {}), ...(result ? { [OTEL_ATTRS.APP_TOOL_RESULT]: typeof result === 'string' ? result.substring(0, 2000) : JSON.stringify(result).substring(0, 2000), 'app.tool.result.hash': this._hashString(typeof result === 'string' ? result : JSON.stringify(result)) } : {}) }; span.setAttributes(attributes); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR, message: errorMessage }); span.end(); this.activeSpans.delete(`${sessionId}_tool_execution_${iterationNumber}`); } /** * End an iteration */ endIteration(sessionId, iterationNumber, success = true, completedAction = null) { const span = this.activeSpans.get(`${sessionId}_iteration_${iterationNumber}`); if (!span) return; span.setAttributes({ 'app.iteration.success': success, 'app.iteration.end_time': Date.now(), ...(completedAction ? { 'app.iteration.completed_action': completedAction } : {}) }); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR }); span.end(); this.activeSpans.delete(`${sessionId}_iteration_${iterationNumber}`); } /** * End the agent loop */ endAgentLoop(sessionId, totalIterations, success = true, completionReason = null) { const span = this.activeSpans.get(`${sessionId}_agent_loop`); if (!span) return; span.setAttributes({ 'app.loop.total_iterations': totalIterations, 'app.loop.success': success, 'app.loop.end_time': Date.now(), ...(completionReason ? { 'app.loop.completion_reason': completionReason } : {}) }); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR }); span.end(); this.activeSpans.delete(`${sessionId}_agent_loop`); } /** * End user message processing */ endUserMessageProcessing(sessionId, success = true) { const span = this.activeSpans.get(`${sessionId}_user_processing`); if (!span) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: No user message processing span found for ${sessionId}`); } return; } if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Ending user message processing span ${span.spanContext().spanId} for ${sessionId}`); } span.setAttributes({ 'app.processing.success': success, 'app.processing.end_time': Date.now() }); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR }); span.end(); this.activeSpans.delete(`${sessionId}_user_processing`); // Clean up the message processing context this.sessionContexts.delete(`${sessionId}_message_processing`); } /** * End the chat session */ endChatSession(sessionId, success = true, totalTokensUsed = 0) { const span = this.sessionSpans.get(sessionId); if (!span) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: No chat session span found for ${sessionId}`); } return; } if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Ending chat session span ${span.spanContext().spanId} for ${sessionId}`); } span.setAttributes({ 'app.session.success': success, 'app.session.total_tokens_used': totalTokensUsed, 'app.session.end_time': Date.now() }); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR }); span.end(); this.sessionSpans.delete(sessionId); // Clean up the session context after ending the span this.sessionContexts.delete(sessionId); } /** * End AI request span */ endAiRequest(sessionId, iterationNumber, success = true) { const span = this.activeSpans.get(`${sessionId}_ai_request_${iterationNumber}`); if (!span) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: No AI request span found for ${sessionId}_ai_request_${iterationNumber}`); } return; } if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Ending AI request span ${span.spanContext().spanId} for ${sessionId}, iteration ${iterationNumber}`); } span.setAttributes({ 'app.ai.request_success': success, 'app.ai.request_end_time': Date.now() }); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR }); span.end(); this.activeSpans.delete(`${sessionId}_ai_request_${iterationNumber}`); } /** * Record a completion attempt */ recordCompletionAttempt(sessionId, success = true, finalResult = null) { const sessionSpan = this.sessionSpans.get(sessionId); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.completion.success': success, 'app.completion.result_length': finalResult ? finalResult.length : 0, 'app.completion.attempt_time': Date.now() } }; if (sessionSpan) { spanOptions.parent = sessionSpan.spanContext(); } const span = this.tracer.startSpan('agent.completion.attempt', spanOptions); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR }); span.end(); return span; } /** * Start image URL processing */ startImageProcessing(sessionId, messageId, imageUrls = [], cleanedMessageLength = 0) { const userProcessingSpan = this.activeSpans.get(`${sessionId}_user_processing`); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.message.id': messageId, 'app.image.urls_found': imageUrls.length, 'app.image.message_cleaned_length': cleanedMessageLength, 'app.image.processing_start_time': Date.now(), 'app.image.urls_list': JSON.stringify(imageUrls).substring(0, 500) } }; if (userProcessingSpan) { spanOptions.parent = userProcessingSpan.spanContext(); } const span = this.tracer.startSpan('content.image.processing', spanOptions); this.activeSpans.set(`${sessionId}_image_processing`, span); return span; } /** * Record image URL validation results */ recordImageValidation(sessionId, validationResults) { const imageProcessingSpan = this.activeSpans.get(`${sessionId}_image_processing`); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.image.validation.total_urls': validationResults.totalUrls || 0, 'app.image.validation.valid_urls': validationResults.validUrls || 0, 'app.image.validation.invalid_urls': validationResults.invalidUrls || 0, 'app.image.validation.redirected_urls': validationResults.redirectedUrls || 0, 'app.image.validation.timeout_urls': validationResults.timeoutUrls || 0, 'app.image.validation.network_errors': validationResults.networkErrors || 0, 'app.image.validation.duration_ms': validationResults.durationMs || 0, 'app.image.validation_time': Date.now() } }; if (imageProcessingSpan) { spanOptions.parent = imageProcessingSpan.spanContext(); } const span = this.tracer.startSpan('content.image.validation', spanOptions); span.setStatus({ code: validationResults.validUrls > 0 ? SpanStatusCode.OK : SpanStatusCode.ERROR, message: `${validationResults.validUrls}/${validationResults.totalUrls} URLs validated successfully` }); span.end(); return span; } /** * End image processing */ endImageProcessing(sessionId, success = true, finalValidUrls = 0) { const span = this.activeSpans.get(`${sessionId}_image_processing`); if (!span) return; span.setAttributes({ 'app.image.processing_success': success, 'app.image.final_valid_urls': finalValidUrls, 'app.image.processing_end_time': Date.now() }); span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR }); span.end(); this.activeSpans.delete(`${sessionId}_image_processing`); } /** * Record AI model errors */ recordAiModelError(sessionId, iterationNumber, errorDetails) { const aiRequestSpan = this.activeSpans.get(`${sessionId}_ai_request_${iterationNumber}`); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.error.type': 'ai_model_error', 'app.error.category': errorDetails.category || 'unknown', // timeout, api_limit, network, etc. 'app.error.message': errorDetails.message?.substring(0, 500) || '', 'app.error.model': errorDetails.model || '', 'app.error.provider': errorDetails.provider || '', 'app.error.status_code': errorDetails.statusCode || 0, 'app.error.retry_attempt': errorDetails.retryAttempt || 0, 'app.error.timestamp': Date.now() } }; if (aiRequestSpan) { spanOptions.parent = aiRequestSpan.spanContext(); } const span = this.tracer.startSpan('ai.generation.error', spanOptions); span.setStatus({ code: SpanStatusCode.ERROR, message: errorDetails.message }); span.end(); return span; } /** * Record tool execution errors */ recordToolError(sessionId, iterationNumber, toolName, errorDetails) { const toolExecutionSpan = this.activeSpans.get(`${sessionId}_tool_execution_${iterationNumber}`); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.error.type': 'tool_execution_error', 'app.error.tool_name': toolName, 'app.error.category': errorDetails.category || 'unknown', // validation, execution, network, filesystem 'app.error.message': errorDetails.message?.substring(0, 500) || '', 'app.error.exit_code': errorDetails.exitCode || 0, 'app.error.signal': errorDetails.signal || '', 'app.error.params': JSON.stringify(errorDetails.params || {}).substring(0, 300), 'app.error.timestamp': Date.now() } }; if (toolExecutionSpan) { spanOptions.parent = toolExecutionSpan.spanContext(); } const span = this.tracer.startSpan('tool.call.error', spanOptions); span.setStatus({ code: SpanStatusCode.ERROR, message: errorDetails.message }); span.end(); return span; } /** * Record session cancellation */ recordSessionCancellation(sessionId, reason = 'user_request', context = {}) { const sessionSpan = this.sessionSpans.get(sessionId); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.cancellation.reason': reason, // user_request, timeout, error, signal 'app.cancellation.context': JSON.stringify(context).substring(0, 300), 'app.cancellation.current_iteration': context.currentIteration || 0, 'app.cancellation.active_tool': context.activeTool || '', 'app.cancellation.timestamp': Date.now() } }; if (sessionSpan) { spanOptions.parent = sessionSpan.spanContext(); } const span = this.tracer.startSpan('messaging.session.cancel', spanOptions); span.setStatus({ code: SpanStatusCode.ERROR, message: `Session cancelled: ${reason}` }); span.end(); return span; } /** * Record token management metrics */ recordTokenMetrics(sessionId, tokenData) { const sessionSpan = this.sessionSpans.get(sessionId); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.tokens.context_window': tokenData.contextWindow || 0, 'app.tokens.current_total': tokenData.currentTotal || 0, 'app.tokens.request_tokens': tokenData.requestTokens || 0, 'app.tokens.response_tokens': tokenData.responseTokens || 0, 'app.tokens.cache_read': tokenData.cacheRead || 0, 'app.tokens.cache_write': tokenData.cacheWrite || 0, 'app.tokens.utilization_percent': tokenData.contextWindow ? Math.round((tokenData.currentTotal / tokenData.contextWindow) * 100) : 0, 'app.tokens.measurement_time': Date.now() } }; if (sessionSpan) { spanOptions.parent = sessionSpan.spanContext(); } const span = this.tracer.startSpan('ai.token.metrics', spanOptions); span.setStatus({ code: SpanStatusCode.OK }); span.end(); return span; } /** * Record history management operations */ recordHistoryOperation(sessionId, operation, details = {}) { const sessionSpan = this.sessionSpans.get(sessionId); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.history.operation': operation, // trim, update, clear, save 'app.history.messages_before': details.messagesBefore || 0, 'app.history.messages_after': details.messagesAfter || 0, 'app.history.messages_removed': details.messagesRemoved || 0, 'app.history.reason': details.reason || '', // max_length, memory_limit, session_reset 'app.history.operation_time': Date.now() } }; if (sessionSpan) { spanOptions.parent = sessionSpan.spanContext(); } const span = this.tracer.startSpan('messaging.history.manage', spanOptions); span.setStatus({ code: SpanStatusCode.OK }); span.end(); return span; } /** * Record system prompt generation metrics */ recordSystemPromptGeneration(sessionId, promptData) { const sessionSpan = this.sessionSpans.get(sessionId); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.prompt.base_length': promptData.baseLength || 0, 'app.prompt.final_length': promptData.finalLength || 0, 'app.prompt.files_added': promptData.filesAdded || 0, 'app.prompt.generation_duration_ms': promptData.generationDurationMs || 0, 'app.prompt.type': promptData.promptType || 'default', 'app.prompt.estimated_tokens': promptData.estimatedTokens || 0, 'app.prompt.generation_time': Date.now() } }; if (sessionSpan) { spanOptions.parent = sessionSpan.spanContext(); } const span = this.tracer.startSpan('ai.prompt.generate', spanOptions); span.setStatus({ code: SpanStatusCode.OK }); span.end(); return span; } /** * Record file system operations */ recordFileSystemOperation(sessionId, operation, details = {}) { const activeSpan = this.activeSpans.get(`${sessionId}_tool_execution_${details.iterationNumber}`) || this.sessionSpans.get(sessionId); const spanOptions = { kind: SpanKind.INTERNAL, attributes: { 'app.session.id': sessionId, 'app.fs.operation': operation, // read, write, create_temp, delete, mkdir 'app.fs.path': details.path?.substring(0, 200) || '', 'app.fs.size_bytes': details.sizeBytes || 0, 'app.fs.duration_ms': details.durationMs || 0, 'app.fs.success': details.success !== false, 'app.fs.error_code': details.errorCode || '', 'app.fs.operation_time': Date.now() } }; if (activeSpan) { spanOptions.parent = activeSpan.spanContext(); } const span = this.tracer.startSpan('fs.operation', spanOptions); span.setStatus({ code: details.success !== false ? SpanStatusCode.OK : SpanStatusCode.ERROR, message: details.errorMessage }); span.end(); return span; } /** * Clean up any remaining active spans for a session */ cleanup(sessionId) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Cleaning up session ${sessionId}`); } // End any remaining active spans const keysToDelete = []; for (const [key, span] of this.activeSpans.entries()) { if (key.includes(sessionId)) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Cleaning up active span ${key}`); } span.setStatus({ code: SpanStatusCode.ERROR, message: 'Session cleanup' }); span.end(); keysToDelete.push(key); } } keysToDelete.forEach(key => this.activeSpans.delete(key)); // Only clean up session span if it still exists (wasn't properly ended by endChatSession) const sessionSpan = this.sessionSpans.get(sessionId); if (sessionSpan) { if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Cleaning up orphaned session span for ${sessionId}`); } sessionSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'Session cleanup - orphaned span' }); sessionSpan.end(); this.sessionSpans.delete(sessionId); } // Clean up all session-related contexts (session, message processing, iterations, AI requests, tool executions) const contextKeysToDelete = []; for (const [key] of this.sessionContexts.entries()) { if (key.includes(sessionId)) { contextKeysToDelete.push(key); } } contextKeysToDelete.forEach(key => this.sessionContexts.delete(key)); if (process.env.DEBUG_CHAT === '1') { console.log(`[DEBUG] AppTracer: Session cleanup completed for ${sessionId}, cleaned ${contextKeysToDelete.length} contexts`); } } } // Export a singleton instance export const appTracer = new AppTracer();