Files
n8n-mcp/src/telemetry/telemetry-manager.ts
Romuald Członkowski 99c5907b71 feat: enhance workflow mutation telemetry for better AI responses (#419)
* feat: add comprehensive telemetry for partial workflow updates

Implement telemetry infrastructure to track workflow mutations from
partial update operations. This enables data-driven improvements to
partial update tooling by capturing:

- Workflow state before and after mutations
- User intent and operation patterns
- Validation results and improvements
- Change metrics (nodes/connections modified)
- Success/failure rates and error patterns

New Components:
- Intent classifier: Categorizes mutation patterns
- Intent sanitizer: Removes PII from user instructions
- Mutation validator: Ensures data quality before tracking
- Mutation tracker: Coordinates validation and metric calculation

Extended Components:
- TelemetryManager: New trackWorkflowMutation() method
- EventTracker: Mutation queue management
- BatchProcessor: Mutation data flushing to Supabase

MCP Tool Enhancements:
- n8n_update_partial_workflow: Added optional 'intent' parameter
- n8n_update_full_workflow: Added optional 'intent' parameter
- Both tools now track mutations asynchronously

Database Schema:
- New workflow_mutations table with 20+ fields
- Comprehensive indexes for efficient querying
- Supports deduplication and data analysis

This telemetry system is:
- Privacy-focused (PII sanitization, anonymized users)
- Non-blocking (async tracking, silent failures)
- Production-ready (batching, retries, circuit breaker)
- Backward compatible (all parameters optional)

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

* fix: correct SQL syntax for expression index in workflow_mutations schema

The expression index for significant changes needs double parentheses
around the arithmetic expression to be valid PostgreSQL syntax.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

* fix: enable RLS policies for workflow_mutations table

Enable Row-Level Security and add policies:
- Allow anonymous (anon) inserts for telemetry data collection
- Allow authenticated reads for data analysis and querying

These policies are required for the telemetry system to function
correctly with Supabase, as the MCP server uses the anon key to
insert mutation data.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

* fix: reduce mutation auto-flush threshold from 5 to 2

Lower the auto-flush threshold for workflow mutations from 5 to 2 to ensure
more timely data persistence. Since mutations are less frequent than regular
telemetry events, a lower threshold provides:

- Faster data persistence (don't wait for 5 mutations)
- Better testing experience (easier to verify with fewer operations)
- Reduced risk of data loss if process exits before threshold
- More responsive telemetry for low-volume mutation scenarios

This complements the existing 5-second periodic flush and process exit
handlers, ensuring mutations are persisted promptly.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

* fix: improve mutation telemetry error logging and diagnostics

Changes:
- Upgrade error logging from debug to warn level for better visibility
- Add diagnostic logging to track mutation processing
- Log telemetry disabled state explicitly
- Add context info (sessionId, intent, operationCount) to error logs
- Remove 'await' from telemetry calls to make them truly non-blocking

This will help identify why mutations aren't being persisted to the
workflow_mutations table despite successful workflow operations.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

* feat: enhance workflow mutation telemetry for better AI responses

Improve workflow mutation tracking to capture comprehensive data that helps provide better responses when users update workflows. This enhancement collects workflow state, user intent, and operation details to enable more context-aware assistance.

Key improvements:
- Reduce auto-flush threshold from 5 to 2 for more reliable mutation tracking
- Add comprehensive workflow and credential sanitization to mutation tracker
- Document intent parameter in workflow update tools for better UX
- Fix mutation queue handling in telemetry manager (flush now handles 3 queues)
- Add extensive unit tests for mutation tracking and validation (35 new tests)

Technical changes:
- mutation-tracker.ts: Multi-layer sanitization (workflow, node, parameter levels)
- batch-processor.ts: Support mutation data flushing to Supabase
- telemetry-manager.ts: Auto-flush mutations at threshold 2, track mutations queue
- handlers-workflow-diff.ts: Track workflow mutations with sanitized data
- Tests: 13 tests for mutation-tracker, 22 tests for mutation-validator

The intent parameter messaging emphasizes user benefit ("helps to return better response") rather than technical implementation details.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to 2.22.16 with telemetry changelog

Updated package.json and package.runtime.json to version 2.22.16.
Added comprehensive CHANGELOG entry documenting workflow mutation
telemetry enhancements for better AI-powered workflow assistance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: resolve TypeScript lint errors in telemetry tests

Fixed type issues in mutation-tracker and mutation-validator tests:
- Import and use MutationToolName enum instead of string literals
- Fix ValidationResult.errors to use proper object structure
- Add UpdateNodeOperation type assertion for operation with nodeName

All TypeScript errors resolved, lint now passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-13 14:21:51 +01:00

377 lines
11 KiB
TypeScript

/**
* Telemetry Manager
* Main telemetry coordinator using modular components
*/
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { TelemetryConfigManager } from './config-manager';
import { TelemetryEventTracker } from './event-tracker';
import { TelemetryBatchProcessor } from './batch-processor';
import { TelemetryPerformanceMonitor } from './performance-monitor';
import { TELEMETRY_BACKEND } from './telemetry-types';
import { TelemetryError, TelemetryErrorType, TelemetryErrorAggregator } from './telemetry-error';
import { logger } from '../utils/logger';
export class TelemetryManager {
private static instance: TelemetryManager;
private supabase: SupabaseClient | null = null;
private configManager: TelemetryConfigManager;
private eventTracker: TelemetryEventTracker;
private batchProcessor: TelemetryBatchProcessor;
private performanceMonitor: TelemetryPerformanceMonitor;
private errorAggregator: TelemetryErrorAggregator;
private isInitialized: boolean = false;
private constructor() {
// Prevent direct instantiation even when TypeScript is bypassed
if (TelemetryManager.instance) {
throw new Error('Use TelemetryManager.getInstance() instead of new TelemetryManager()');
}
this.configManager = TelemetryConfigManager.getInstance();
this.errorAggregator = new TelemetryErrorAggregator();
this.performanceMonitor = new TelemetryPerformanceMonitor();
// Initialize event tracker with callbacks
this.eventTracker = new TelemetryEventTracker(
() => this.configManager.getUserId(),
() => this.isEnabled()
);
// Initialize batch processor (will be configured after Supabase init)
this.batchProcessor = new TelemetryBatchProcessor(
null,
() => this.isEnabled()
);
// Delay initialization to first use, not constructor
// this.initialize();
}
static getInstance(): TelemetryManager {
if (!TelemetryManager.instance) {
TelemetryManager.instance = new TelemetryManager();
}
return TelemetryManager.instance;
}
/**
* Ensure telemetry is initialized before use
*/
private ensureInitialized(): void {
if (!this.isInitialized && this.configManager.isEnabled()) {
this.initialize();
}
}
/**
* Initialize telemetry if enabled
*/
private initialize(): void {
if (!this.configManager.isEnabled()) {
logger.debug('Telemetry disabled by user preference');
return;
}
// Use hardcoded credentials for zero-configuration telemetry
// Environment variables can override for development/testing
const supabaseUrl = process.env.SUPABASE_URL || TELEMETRY_BACKEND.URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || TELEMETRY_BACKEND.ANON_KEY;
try {
this.supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
},
realtime: {
params: {
eventsPerSecond: 1,
},
},
});
// Update batch processor with Supabase client
this.batchProcessor = new TelemetryBatchProcessor(
this.supabase,
() => this.isEnabled()
);
this.batchProcessor.start();
this.isInitialized = true;
logger.debug('Telemetry initialized successfully');
} catch (error) {
const telemetryError = new TelemetryError(
TelemetryErrorType.INITIALIZATION_ERROR,
'Failed to initialize telemetry',
{ error: error instanceof Error ? error.message : String(error) }
);
this.errorAggregator.record(telemetryError);
telemetryError.log();
this.isInitialized = false;
}
}
/**
* Track a tool usage event
*/
trackToolUsage(toolName: string, success: boolean, duration?: number): void {
this.ensureInitialized();
this.performanceMonitor.startOperation('trackToolUsage');
this.eventTracker.trackToolUsage(toolName, success, duration);
this.eventTracker.updateToolSequence(toolName);
this.performanceMonitor.endOperation('trackToolUsage');
}
/**
* Track workflow creation
*/
async trackWorkflowCreation(workflow: any, validationPassed: boolean): Promise<void> {
this.ensureInitialized();
this.performanceMonitor.startOperation('trackWorkflowCreation');
try {
await this.eventTracker.trackWorkflowCreation(workflow, validationPassed);
// Auto-flush workflows to prevent data loss
await this.flush();
} catch (error) {
const telemetryError = error instanceof TelemetryError
? error
: new TelemetryError(
TelemetryErrorType.UNKNOWN_ERROR,
'Failed to track workflow',
{ error: String(error) }
);
this.errorAggregator.record(telemetryError);
} finally {
this.performanceMonitor.endOperation('trackWorkflowCreation');
}
}
/**
* Track workflow mutation from partial updates
*/
async trackWorkflowMutation(data: any): Promise<void> {
this.ensureInitialized();
if (!this.isEnabled()) {
logger.debug('Telemetry disabled, skipping mutation tracking');
return;
}
this.performanceMonitor.startOperation('trackWorkflowMutation');
try {
const { mutationTracker } = await import('./mutation-tracker.js');
const userId = this.configManager.getUserId();
const mutationRecord = await mutationTracker.processMutation(data, userId);
if (mutationRecord) {
// Queue for batch processing
this.eventTracker.enqueueMutation(mutationRecord);
// Auto-flush if queue reaches threshold
// Lower threshold (2) for mutations since they're less frequent than regular events
const queueSize = this.eventTracker.getMutationQueueSize();
if (queueSize >= 2) {
await this.flushMutations();
}
}
} catch (error) {
const telemetryError = error instanceof TelemetryError
? error
: new TelemetryError(
TelemetryErrorType.UNKNOWN_ERROR,
'Failed to track workflow mutation',
{ error: String(error) }
);
this.errorAggregator.record(telemetryError);
logger.debug('Error tracking workflow mutation:', error);
} finally {
this.performanceMonitor.endOperation('trackWorkflowMutation');
}
}
/**
* Track an error event
*/
trackError(errorType: string, context: string, toolName?: string, errorMessage?: string): void {
this.ensureInitialized();
this.eventTracker.trackError(errorType, context, toolName, errorMessage);
}
/**
* Track a generic event
*/
trackEvent(eventName: string, properties: Record<string, any>): void {
this.ensureInitialized();
this.eventTracker.trackEvent(eventName, properties);
}
/**
* Track session start
*/
trackSessionStart(): void {
this.ensureInitialized();
this.eventTracker.trackSessionStart();
}
/**
* Track search queries
*/
trackSearchQuery(query: string, resultsFound: number, searchType: string): void {
this.eventTracker.trackSearchQuery(query, resultsFound, searchType);
}
/**
* Track validation details
*/
trackValidationDetails(nodeType: string, errorType: string, details: Record<string, any>): void {
this.eventTracker.trackValidationDetails(nodeType, errorType, details);
}
/**
* Track tool sequences
*/
trackToolSequence(previousTool: string, currentTool: string, timeDelta: number): void {
this.eventTracker.trackToolSequence(previousTool, currentTool, timeDelta);
}
/**
* Track node configuration
*/
trackNodeConfiguration(nodeType: string, propertiesSet: number, usedDefaults: boolean): void {
this.eventTracker.trackNodeConfiguration(nodeType, propertiesSet, usedDefaults);
}
/**
* Track performance metrics
*/
trackPerformanceMetric(operation: string, duration: number, metadata?: Record<string, any>): void {
this.eventTracker.trackPerformanceMetric(operation, duration, metadata);
}
/**
* Flush queued events to Supabase
*/
async flush(): Promise<void> {
this.ensureInitialized();
if (!this.isEnabled() || !this.supabase) return;
this.performanceMonitor.startOperation('flush');
// Get queued data from event tracker
const events = this.eventTracker.getEventQueue();
const workflows = this.eventTracker.getWorkflowQueue();
const mutations = this.eventTracker.getMutationQueue();
// Clear queues immediately to prevent duplicate processing
this.eventTracker.clearEventQueue();
this.eventTracker.clearWorkflowQueue();
this.eventTracker.clearMutationQueue();
try {
// Use batch processor to flush
await this.batchProcessor.flush(events, workflows, mutations);
} catch (error) {
const telemetryError = error instanceof TelemetryError
? error
: new TelemetryError(
TelemetryErrorType.NETWORK_ERROR,
'Failed to flush telemetry',
{ error: String(error) },
true // Retryable
);
this.errorAggregator.record(telemetryError);
telemetryError.log();
} finally {
const duration = this.performanceMonitor.endOperation('flush');
if (duration > 100) {
logger.debug(`Telemetry flush took ${duration.toFixed(2)}ms`);
}
}
}
/**
* Flush queued mutations only
*/
async flushMutations(): Promise<void> {
this.ensureInitialized();
if (!this.isEnabled() || !this.supabase) return;
const mutations = this.eventTracker.getMutationQueue();
this.eventTracker.clearMutationQueue();
if (mutations.length > 0) {
await this.batchProcessor.flush([], [], mutations);
}
}
/**
* Check if telemetry is enabled
*/
private isEnabled(): boolean {
return this.isInitialized && this.configManager.isEnabled();
}
/**
* Disable telemetry
*/
disable(): void {
this.configManager.disable();
this.batchProcessor.stop();
this.isInitialized = false;
this.supabase = null;
}
/**
* Enable telemetry
*/
enable(): void {
this.configManager.enable();
this.initialize();
}
/**
* Get telemetry status
*/
getStatus(): string {
return this.configManager.getStatus();
}
/**
* Get comprehensive telemetry metrics
*/
getMetrics() {
return {
status: this.isEnabled() ? 'enabled' : 'disabled',
initialized: this.isInitialized,
tracking: this.eventTracker.getStats(),
processing: this.batchProcessor.getMetrics(),
errors: this.errorAggregator.getStats(),
performance: this.performanceMonitor.getDetailedReport(),
overhead: this.performanceMonitor.getTelemetryOverhead()
};
}
/**
* Reset singleton instance (for testing purposes)
*/
static resetInstance(): void {
TelemetryManager.instance = undefined as any;
(global as any).__telemetryManager = undefined;
}
}
// Create a global singleton to ensure only one instance across all imports
const globalAny = global as any;
if (!globalAny.__telemetryManager) {
globalAny.__telemetryManager = TelemetryManager.getInstance();
}
// Export singleton instance
export const telemetry = globalAny.__telemetryManager as TelemetryManager;