"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