mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 13:33:11 +00:00
chore: add pre-built dist folder for npx usage
This commit is contained in:
committed by
Romuald Członkowski
parent
a70d96a373
commit
5057481e70
356
dist/telemetry/event-tracker.js
vendored
Normal file
356
dist/telemetry/event-tracker.js
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user