mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
356 lines
13 KiB
JavaScript
356 lines
13 KiB
JavaScript
"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
|