UNPKG

n8n

Version:

n8n Workflow Automation Tool

440 lines • 18.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Telemetry = void 0; const backend_common_1 = require("@n8n/backend-common"); const backend_network_1 = require("@n8n/backend-network"); const config_1 = require("@n8n/config"); const db_1 = require("@n8n/db"); const decorators_1 = require("@n8n/decorators"); const di_1 = require("@n8n/di"); const n8n_core_1 = require("n8n-core"); const constants_1 = require("../constants"); const license_1 = require("../license"); const posthog_1 = require("../posthog"); const source_control_preferences_service_ee_1 = require("../modules/source-control.ee/source-control-preferences.service.ee"); let Telemetry = class Telemetry { constructor(logger, postHog, license, instanceSettings, workflowRepository, globalConfig, errorReporter, outboundHttp) { this.logger = logger; this.postHog = postHog; this.license = license; this.instanceSettings = instanceSettings; this.workflowRepository = workflowRepository; this.globalConfig = globalConfig; this.errorReporter = errorReporter; this.outboundHttp = outboundHttp; this.executionCountsBuffer = {}; this.apiInvocationsBuffer = {}; this.agentExecutionCountsBuffer = {}; this.agentSessionMetricsBuffer = {}; } sanitizeTelemetryProperties(obj, depth = 0, maxDepth = 10) { try { const result = {}; for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined) { continue; } else if (typeof value === 'boolean') { result[key] = value ? 'true' : 'false'; } else if (typeof value === 'number') { result[key] = value; } else if (typeof value === 'string') { result[key] = value; } else if (Array.isArray(value)) { result[key] = JSON.stringify(value); } else if (typeof value === 'object' && value.constructor === Object) { if (depth >= maxDepth) { result[key] = JSON.stringify(value); } else { Object.assign(result, this.sanitizeTelemetryProperties(value, depth + 1, maxDepth)); } } else { continue; } } return result; } catch (e) { this.logger.error('Error sanitizing telemetry properties', { error: e, object: obj }); return {}; } } async init() { const { enabled, backendConfig } = this.globalConfig.diagnostics; if (enabled) { const [key, dataPlaneUrl] = backendConfig.split(';'); if (!key || !dataPlaneUrl) { this.logger.warn('Diagnostics backend config is invalid'); return; } const logLevel = this.globalConfig.logging.level; const { default: RudderStack } = await import('@rudderstack/rudder-sdk-node'); const { httpAgent, httpsAgent } = this.outboundHttp .transport({ ssrf: 'disabled', }) .getNodeAgent(); const axiosConfig = { httpAgent, httpsAgent, headers: { 'Content-Type': 'application/json' }, }; this.rudderStack = new RudderStack(key, { axiosConfig, logLevel, dataPlaneUrl, gzip: false, errorHandler: (error) => { this.errorReporter.error(error); }, }); this.startPulse(); } } startPulse() { this.pulseIntervalReference = setInterval(async () => { void this.pulse(); }, 6 * 60 * 60 * 1000); } async pulse() { if (!this.rudderStack) { return; } this.flushWorkflowExecutionCounts(); this.flushAgentExecutionCounts(); this.flushAgentSessionMetrics(); for (const userId of Object.keys(this.apiInvocationsBuffer)) { const entry = this.apiInvocationsBuffer[userId]; if (entry.total_calls > 0) { this.track('Public API usage', { user_id: userId, total_calls: entry.total_calls, first: entry.first, endpoints: JSON.stringify(entry.endpoints), user_agents: JSON.stringify(entry.user_agents), }); } } this.apiInvocationsBuffer = {}; const sourceControlPreferences = di_1.Container.get(source_control_preferences_service_ee_1.SourceControlPreferencesService).getPreferences(); const pulsePacket = { plan_name_current: this.license.getPlanName(), quota: this.license.getTriggerLimit(), usage: await this.workflowRepository.getActiveTriggerCount(), role_count: await di_1.Container.get(db_1.UserRepository).countUsersByRole(), source_control_set_up: di_1.Container.get(source_control_preferences_service_ee_1.SourceControlPreferencesService).isSourceControlSetup(), branchName: sourceControlPreferences.branchName, read_only_instance: sourceControlPreferences.branchReadOnly, team_projects: (await di_1.Container.get(db_1.ProjectRepository).getProjectCounts()).team, project_role_count: await di_1.Container.get(db_1.ProjectRelationRepository).countUsersByRole(), }; this.track('pulse', pulsePacket); } flushWorkflowExecutionCounts() { const workflowIdsToReport = Object.keys(this.executionCountsBuffer).filter((workflowId) => { const data = this.executionCountsBuffer[workflowId]; const sum = (data.manual_error?.count ?? 0) + (data.manual_success?.count ?? 0) + (data.prod_error?.count ?? 0) + (data.prod_success?.count ?? 0) + (data.manual_crashed?.count ?? 0) + (data.prod_crashed?.count ?? 0); return sum > 0; }); for (const workflowId of workflowIdsToReport) { this.track('Workflow execution count', { event_version: '2', workflow_id: workflowId, ...this.executionCountsBuffer[workflowId], }); } this.executionCountsBuffer = {}; } getAgentExecutionCountsBufferKey(agentId, userId) { return userId ? `${agentId}:${userId}` : agentId; } flushAgentExecutionCounts() { const keysToReport = Object.keys(this.agentExecutionCountsBuffer).filter((bufferKey) => { const data = this.agentExecutionCountsBuffer[bufferKey]; return data.message_count + data.token_count + data.tool_call_count > 0; }); for (const bufferKey of keysToReport) { const { agent_id, user_id, ...counts } = this.agentExecutionCountsBuffer[bufferKey]; this.track('Agent execution count', { event_version: '1', agent_id, ...(user_id ? { user_id } : {}), ...counts, }); } this.agentExecutionCountsBuffer = {}; } getAgentSessionMetricsBufferKey(properties) { return [ properties.agent_id, properties.run_type, properties.turn_status, JSON.stringify(properties.configuration), ].join(':'); } flushAgentSessionMetrics() { for (const bucket of Object.values(this.agentSessionMetricsBuffer)) { const sessions = Object.values(bucket.sessions); if (sessions.length === 0) continue; const latencyMsSum = sessions.reduce((total, session) => total + session.latency_ms, 0); const costSum = sessions.reduce((total, session) => total + session.cost, 0); const toolCallCountSum = sessions.reduce((total, session) => total + session.tool_call_count, 0); const numSkillsSum = sessions.reduce((total, session) => total + session.num_skills, 0); const turnCount = sessions.reduce((total, session) => total + session.turn_count, 0); this.track('Agent session metrics', { event_version: '1', agent_id: bucket.agent_id, ...(bucket.agent_type ? { agent_type: bucket.agent_type } : {}), ...bucket.configuration, run_type: bucket.run_type, turn_status: bucket.turn_status, session_count: sessions.length, turn_count: turnCount, latency_ms_sum: latencyMsSum, cost_sum: costSum, tool_call_count_sum: toolCallCountSum, num_skills_sum: numSkillsSum, }); } this.agentSessionMetricsBuffer = {}; } trackWorkflowExecution(properties) { if (this.rudderStack) { const execTime = new Date(); const workflowId = properties.workflow_id; this.executionCountsBuffer[workflowId] = this.executionCountsBuffer[workflowId] ?? { user_id: properties.user_id, }; let key; if (properties.crashed) { key = `${properties.is_manual ? 'manual' : 'prod'}_crashed`; } else { key = `${properties.is_manual ? 'manual' : 'prod'}_${properties.success ? 'success' : 'error'}`; } this.addExecutionTrackData(workflowId, key, execTime); const executionStatus = properties.crashed ? 'crashed' : properties.success ? 'success' : 'error'; const executionMode = properties.is_manual ? 'manual' : 'prod'; if (properties.execution_source === 'instance_ai') { const instanceAiDataType = properties.mock_data_sources ? 'mock' : 'real'; const sourceKey = `instance_ai_${instanceAiDataType}_${executionMode}_${executionStatus}`; this.addExecutionTrackData(workflowId, sourceKey, execTime); } if (properties.used_end_user_credentials) { this.track('Workflow execution with end-user credentials', properties); } if (!properties.success && properties.is_manual && properties.error_node_type?.startsWith('n8n-nodes-base')) { this.track('Workflow execution errored', properties); } } } addExecutionTrackData(workflowId, key, execTime) { const executionTrackData = this.executionCountsBuffer[workflowId][key]; if (!executionTrackData) { this.executionCountsBuffer[workflowId][key] = { count: 1, first: execTime, }; } else { executionTrackData.count++; } } trackAgentExecution(properties) { if (!this.rudderStack) return; const { agent_id, user_id, message_count = 0, token_count = 0, tool_call_count = 0, } = properties; const bufferKey = this.getAgentExecutionCountsBufferKey(agent_id, user_id); this.agentExecutionCountsBuffer[bufferKey] = this.agentExecutionCountsBuffer[bufferKey] ?? { agent_id, ...(user_id ? { user_id } : {}), message_count: 0, token_count: 0, tool_call_count: 0, }; const agentExecutionCounts = this.agentExecutionCountsBuffer[bufferKey]; agentExecutionCounts.message_count += message_count; agentExecutionCounts.token_count += token_count; agentExecutionCounts.tool_call_count += tool_call_count; } trackAgentTurnFinished(properties) { if (!this.rudderStack) return; const bufferKey = this.getAgentSessionMetricsBufferKey(properties); this.agentSessionMetricsBuffer[bufferKey] = this.agentSessionMetricsBuffer[bufferKey] ?? { agent_id: properties.agent_id, agent_type: properties.agent_type, run_type: properties.run_type, turn_status: properties.turn_status, configuration: properties.configuration, sessions: {}, }; const bucket = this.agentSessionMetricsBuffer[bufferKey]; const session = bucket.sessions[properties.thread_id] ?? { latency_ms: 0, cost: 0, tool_call_count: 0, num_skills: properties.configuration.num_skills, turn_count: 0, }; session.latency_ms += properties.latency_ms; session.cost += properties.cost; session.tool_call_count += properties.tool_call_count; session.turn_count++; bucket.sessions[properties.thread_id] = session; } trackApiInvocation(properties) { if (!this.rudderStack) return; const { user_id, path, method, user_agent } = properties; this.apiInvocationsBuffer[user_id] = this.apiInvocationsBuffer[user_id] ?? { total_calls: 0, first: new Date(), endpoints: {}, user_agents: {}, }; const entry = this.apiInvocationsBuffer[user_id]; entry.total_calls++; const endpointKey = `${method} ${path}`; entry.endpoints[endpointKey] = (entry.endpoints[endpointKey] ?? 0) + 1; if (user_agent) { entry.user_agents[user_agent] = (entry.user_agents[user_agent] ?? 0) + 1; } } async stopTracking() { clearInterval(this.pulseIntervalReference); await Promise.all([this.postHog.stop(), this.rudderStack?.flush()]); } groupIdentify({ userId, traits, }) { const { instanceId } = this.instanceSettings; if (!instanceId) return; if (this.postHog) { this.postHog.groupIdentify({ ...(userId && { distinctId: `${instanceId}#${userId}` }), instanceId, properties: traits, }); } if (this.rudderStack) { this.rudderStack.group({ groupId: instanceId, userId: userId ? `${instanceId}#${userId}` : instanceId, traits, context: { ip: '0.0.0.0', }, }); } } identify(traits, userId) { const { instanceId } = this.instanceSettings; if (!instanceId) return; if (this.rudderStack) { this.rudderStack.identify({ userId: userId ? `${instanceId}#${userId}` : instanceId, traits: { ...traits, instanceId }, context: { ip: '0.0.0.0', }, }); } if (this.postHog && userId) { this.postHog.identify({ distinctId: `${instanceId}#${userId}`, properties: traits, }); } } setUserCloudId(userCloudId) { this.userCloudId = userCloudId; } track(event, properties = {}) { const eventName = typeof event === 'string' ? event : event.name; if (typeof event !== 'string') { const validationError = event.getValidationError(properties); if (validationError) this.logger.warn(validationError); } if (!this.rudderStack) { return; } const { instanceId } = this.instanceSettings; const { user_id } = properties; const updatedProperties = { ...properties, instance_id: instanceId, user_id: user_id ?? undefined, version_cli: constants_1.N8N_VERSION, }; const payload = { userId: `${instanceId}${user_id ? `#${user_id}` : ''}`, event: eventName, properties: updatedProperties, context: this.userCloudId ? { traits: { user_cloud_id: this.userCloudId } } : {}, }; const rudderStackPayload = { ...payload, context: { ...payload.context, ip: '0.0.0.0' }, }; const payloadSize = Buffer.byteLength(JSON.stringify(rudderStackPayload), 'utf8'); const maxPayloadSize = 32 << 10; if (payloadSize > maxPayloadSize) { return; } this.postHog?.track(payload); return this.rudderStack.track(rudderStackPayload); } getCountsBuffer() { return this.executionCountsBuffer; } getApiInvocationsBuffer() { return this.apiInvocationsBuffer; } getAgentExecutionCountsBuffer() { return this.agentExecutionCountsBuffer; } getAgentSessionMetricsBuffer() { return this.agentSessionMetricsBuffer; } }; exports.Telemetry = Telemetry; __decorate([ (0, decorators_1.OnShutdown)(constants_1.LOWEST_SHUTDOWN_PRIORITY), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], Telemetry.prototype, "stopTracking", null); exports.Telemetry = Telemetry = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, posthog_1.PostHogClient, license_1.License, n8n_core_1.InstanceSettings, db_1.WorkflowRepository, config_1.GlobalConfig, n8n_core_1.ErrorReporter, backend_network_1.OutboundHttp]) ], Telemetry); //# sourceMappingURL=index.js.map