Skip to main content
Glama
event-tracker.js13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TelemetryEventTracker = void 0; const workflow_sanitizer_1 = require("./workflow-sanitizer"); const rate_limiter_1 = require("./rate-limiter"); const event_validator_1 = require("./event-validator"); const telemetry_error_1 = require("./telemetry-error"); const logger_1 = require("../utils/logger"); const fs_1 = require("fs"); const path_1 = require("path"); const error_sanitization_utils_1 = require("./error-sanitization-utils"); class TelemetryEventTracker { constructor(getUserId, isEnabled) { this.getUserId = getUserId; this.isEnabled = isEnabled; this.eventQueue = []; this.workflowQueue = []; this.mutationQueue = []; this.previousToolTimestamp = 0; this.performanceMetrics = new Map(); this.rateLimiter = new rate_limiter_1.TelemetryRateLimiter(); this.validator = new event_validator_1.TelemetryEventValidator(); } trackToolUsage(toolName, success, duration) { if (!this.isEnabled()) return; if (!this.rateLimiter.allow()) { logger_1.logger.debug(`Rate limited: tool_used event for ${toolName}`); return; } if (duration !== undefined) { this.recordPerformanceMetric(toolName, duration); } const event = { user_id: this.getUserId(), event: 'tool_used', properties: { tool: toolName.replace(/[^a-zA-Z0-9_-]/g, '_'), success, duration: duration || 0, } }; const validated = this.validator.validateEvent(event); if (validated) { this.eventQueue.push(validated); } } async trackWorkflowCreation(workflow, validationPassed) { if (!this.isEnabled()) return; if (!this.rateLimiter.allow()) { logger_1.logger.debug('Rate limited: workflow creation event'); return; } if (!validationPassed) { this.trackEvent('workflow_validation_failed', { nodeCount: workflow.nodes?.length || 0, }); return; } try { const sanitized = workflow_sanitizer_1.WorkflowSanitizer.sanitizeWorkflow(workflow); const telemetryData = { user_id: this.getUserId(), workflow_hash: sanitized.workflowHash, node_count: sanitized.nodeCount, node_types: sanitized.nodeTypes, has_trigger: sanitized.hasTrigger, has_webhook: sanitized.hasWebhook, complexity: sanitized.complexity, sanitized_workflow: { nodes: sanitized.nodes, connections: sanitized.connections, }, }; const validated = this.validator.validateWorkflow(telemetryData); if (validated) { this.workflowQueue.push(validated); this.trackEvent('workflow_created', { nodeCount: sanitized.nodeCount, nodeTypes: sanitized.nodeTypes.length, complexity: sanitized.complexity, hasTrigger: sanitized.hasTrigger, hasWebhook: sanitized.hasWebhook, }); } } catch (error) { logger_1.logger.debug('Failed to track workflow creation:', error); throw new telemetry_error_1.TelemetryError(telemetry_error_1.TelemetryErrorType.VALIDATION_ERROR, 'Failed to sanitize workflow', { error: error instanceof Error ? error.message : String(error) }); } } trackError(errorType, context, toolName, errorMessage) { if (!this.isEnabled()) return; this.trackEvent('error_occurred', { errorType: this.sanitizeErrorType(errorType), context: this.sanitizeContext(context), tool: toolName ? toolName.replace(/[^a-zA-Z0-9_-]/g, '_') : undefined, error: errorMessage ? this.sanitizeErrorMessage(errorMessage) : undefined, mcpMode: process.env.MCP_MODE || 'stdio', platform: process.platform }, false); } trackEvent(eventName, properties, checkRateLimit = true) { if (!this.isEnabled()) return; if (checkRateLimit && !this.rateLimiter.allow()) { logger_1.logger.debug(`Rate limited: ${eventName} event`); return; } const event = { user_id: this.getUserId(), event: eventName, properties, }; const validated = this.validator.validateEvent(event); if (validated) { this.eventQueue.push(validated); } } trackSessionStart(startupData) { if (!this.isEnabled()) return; this.trackEvent('session_start', { version: this.getPackageVersion(), platform: process.platform, arch: process.arch, nodeVersion: process.version, isDocker: process.env.IS_DOCKER === 'true', cloudPlatform: this.detectCloudPlatform(), mcpMode: process.env.MCP_MODE || 'stdio', startupDurationMs: startupData?.durationMs, checkpointsPassed: startupData?.checkpoints, startupErrorCount: startupData?.errorCount || 0, }); } trackStartupComplete() { if (!this.isEnabled()) return; this.trackEvent('startup_completed', { version: this.getPackageVersion(), }); } detectCloudPlatform() { if (process.env.RAILWAY_ENVIRONMENT) return 'railway'; if (process.env.RENDER) return 'render'; if (process.env.FLY_APP_NAME) return 'fly'; if (process.env.HEROKU_APP_NAME) return 'heroku'; if (process.env.AWS_EXECUTION_ENV) return 'aws'; if (process.env.KUBERNETES_SERVICE_HOST) return 'kubernetes'; if (process.env.GOOGLE_CLOUD_PROJECT) return 'gcp'; if (process.env.AZURE_FUNCTIONS_ENVIRONMENT) return 'azure'; return null; } trackSearchQuery(query, resultsFound, searchType) { if (!this.isEnabled()) return; this.trackEvent('search_query', { query: query.substring(0, 100), resultsFound, searchType, hasResults: resultsFound > 0, isZeroResults: resultsFound === 0 }); } trackValidationDetails(nodeType, errorType, details) { if (!this.isEnabled()) return; this.trackEvent('validation_details', { nodeType: nodeType.replace(/[^a-zA-Z0-9_.-]/g, '_'), errorType: this.sanitizeErrorType(errorType), errorCategory: this.categorizeError(errorType), details }); } trackToolSequence(previousTool, currentTool, timeDelta) { if (!this.isEnabled()) return; this.trackEvent('tool_sequence', { previousTool: previousTool.replace(/[^a-zA-Z0-9_-]/g, '_'), currentTool: currentTool.replace(/[^a-zA-Z0-9_-]/g, '_'), timeDelta: Math.min(timeDelta, 300000), isSlowTransition: timeDelta > 10000, sequence: `${previousTool}->${currentTool}` }); } trackNodeConfiguration(nodeType, propertiesSet, usedDefaults) { if (!this.isEnabled()) return; this.trackEvent('node_configuration', { nodeType: nodeType.replace(/[^a-zA-Z0-9_.-]/g, '_'), propertiesSet, usedDefaults, complexity: this.categorizeConfigComplexity(propertiesSet) }); } trackPerformanceMetric(operation, duration, metadata) { if (!this.isEnabled()) return; this.recordPerformanceMetric(operation, duration); this.trackEvent('performance_metric', { operation: operation.replace(/[^a-zA-Z0-9_-]/g, '_'), duration, isSlow: duration > 1000, isVerySlow: duration > 5000, metadata }); } updateToolSequence(toolName) { if (this.previousTool) { const timeDelta = Date.now() - this.previousToolTimestamp; this.trackToolSequence(this.previousTool, toolName, timeDelta); } this.previousTool = toolName; this.previousToolTimestamp = Date.now(); } getEventQueue() { return [...this.eventQueue]; } getWorkflowQueue() { return [...this.workflowQueue]; } getMutationQueue() { return [...this.mutationQueue]; } clearEventQueue() { this.eventQueue = []; } clearWorkflowQueue() { this.workflowQueue = []; } clearMutationQueue() { this.mutationQueue = []; } enqueueMutation(mutation) { if (!this.isEnabled()) return; this.mutationQueue.push(mutation); } getMutationQueueSize() { return this.mutationQueue.length; } getStats() { return { rateLimiter: this.rateLimiter.getStats(), validator: this.validator.getStats(), eventQueueSize: this.eventQueue.length, workflowQueueSize: this.workflowQueue.length, mutationQueueSize: this.mutationQueue.length, performanceMetrics: this.getPerformanceStats() }; } recordPerformanceMetric(operation, duration) { if (!this.performanceMetrics.has(operation)) { this.performanceMetrics.set(operation, []); } const metrics = this.performanceMetrics.get(operation); metrics.push(duration); if (metrics.length > 100) { metrics.shift(); } } getPerformanceStats() { const stats = {}; for (const [operation, durations] of this.performanceMetrics.entries()) { if (durations.length === 0) continue; const sorted = [...durations].sort((a, b) => a - b); const sum = sorted.reduce((a, b) => a + b, 0); stats[operation] = { count: sorted.length, min: sorted[0], max: sorted[sorted.length - 1], avg: Math.round(sum / sorted.length), p50: sorted[Math.floor(sorted.length * 0.5)], p95: sorted[Math.floor(sorted.length * 0.95)], p99: sorted[Math.floor(sorted.length * 0.99)] }; } return stats; } categorizeError(errorType) { const lowerError = errorType.toLowerCase(); if (lowerError.includes('type')) return 'type_error'; if (lowerError.includes('validation')) return 'validation_error'; if (lowerError.includes('required')) return 'required_field_error'; if (lowerError.includes('connection')) return 'connection_error'; if (lowerError.includes('expression')) return 'expression_error'; return 'other_error'; } categorizeConfigComplexity(propertiesSet) { if (propertiesSet === 0) return 'defaults_only'; if (propertiesSet <= 3) return 'simple'; if (propertiesSet <= 10) return 'moderate'; return 'complex'; } getPackageVersion() { try { const possiblePaths = [ (0, path_1.resolve)(__dirname, '..', '..', 'package.json'), (0, path_1.resolve)(process.cwd(), 'package.json'), (0, path_1.resolve)(__dirname, '..', '..', '..', 'package.json') ]; for (const packagePath of possiblePaths) { if ((0, fs_1.existsSync)(packagePath)) { const packageJson = JSON.parse((0, fs_1.readFileSync)(packagePath, 'utf-8')); if (packageJson.version) { return packageJson.version; } } } return 'unknown'; } catch (error) { logger_1.logger.debug('Failed to get package version:', error); return 'unknown'; } } sanitizeErrorType(errorType) { return errorType.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 50); } sanitizeContext(context) { let sanitized = context .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]') .replace(/\b[a-zA-Z0-9_-]{32,}/g, '[KEY]') .replace(/(https?:\/\/)([^\s\/]+)(\/[^\s]*)?/gi, (match, protocol, domain, path) => { return '[URL]' + (path || ''); }); if (sanitized.length > 100) { sanitized = sanitized.substring(0, 100); } return sanitized; } sanitizeErrorMessage(errorMessage) { return (0, error_sanitization_utils_1.sanitizeErrorMessageCore)(errorMessage); } } exports.TelemetryEventTracker = TelemetryEventTracker; //# sourceMappingURL=event-tracker.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/czlonkowski/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server