From c5b126768632c26bcb8ba40f2aa15f0dae958b72 Mon Sep 17 00:00:00 2001 From: B Harper Date: Thu, 20 Nov 2025 14:34:50 +1100 Subject: [PATCH] feat: Add anonymous telemetry with GA4 integration - Created TelemetryService with Google Analytics 4 - Track extension lifecycle (activation/deactivation) - Track chat events (opened, messages sent/received) - Track connection errors for debugging - Respects VS Code telemetry settings - Added privacy disclosure to README - Client ID persists across updates - GDPR compliant, no PII collected --- README.md | 32 ++++++ package.json | 2 +- src/extension.ts | 16 ++- src/telemetry/telemetryService.ts | 159 +++++++++++++++++++++++++++++ src/webview/chatWebviewProvider.ts | 19 +++- 5 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 src/telemetry/telemetryService.ts diff --git a/README.md b/README.md index e921b3a..679d6c0 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,38 @@ Access all workspace chats in one browser tab at `http://localhost:3737/HumanAge Press F5 to debug - dev mode uses port 3738, production uses 3737. No conflicts. +## Privacy & Telemetry + +This extension collects **anonymous usage data** to help improve the product: + +**What we track:** +- Extension activation/deactivation +- Feature usage (chat opened, messages sent/received) +- Error diagnostics (error types, not content) +- Session metrics (message counts, not content) +- Extension version, VS Code version, OS platform +- Days since installation + +**What we DON'T track:** +- ❌ Your message content +- ❌ Your name, email, or any personal data +- ❌ Workspace paths or file names +- ❌ Any identifiable information + +**Your privacy:** +- Respects VS Code's telemetry setting +- To disable: Settings → Telemetry → Level → Off +- Fully GDPR compliant +- Uses Google Analytics 4 for anonymous metrics + +**Why telemetry?** +- Helps us understand which features are used +- Identifies bugs and errors to fix +- Measures engagement and retention +- Guides future development priorities + +For questions: [GitHub Issues](https://github.com/3DTek-xyz/HumanAgent-MCP/issues) + ## More Info See [README-Additional.md](README-Additional.md) for technical details diff --git a/package.json b/package.json index 85a2b03..46a04e2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "humanagent-mcp", "displayName": "HumanAgent MCP", "description": "MCP server for chatting with a human agent", - "version": "0.0.15", + "version": "0.0.16", "publisher": "3DTek-xyz", "icon": "HumanAgent_Icon_Square.png", "repository": { diff --git a/src/extension.ts b/src/extension.ts index 5ba6d4a..c49857b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,12 +7,14 @@ import { ChatTreeProvider } from './providers/chatTreeProvider'; import { ChatWebviewProvider } from './webview/chatWebviewProvider'; import { McpConfigManager } from './mcp/mcpConfigManager'; import { ServerManager } from './serverManager'; +import { TelemetryService } from './telemetry/telemetryService'; let chatTreeProvider: ChatTreeProvider; let mcpConfigManager: McpConfigManager; let workspaceSessionId: string; let serverManager: ServerManager; let SERVER_PORT: number; // Dynamic port: 3738 for dev, 3737 for production +let telemetryService: TelemetryService; // MCP Server Definition Provider for VS Code native MCP integration class HumanAgentMcpProvider implements vscode.McpServerDefinitionProvider { @@ -114,6 +116,10 @@ async function restoreSessionName(context: vscode.ExtensionContext, sessionId: s export async function activate(context: vscode.ExtensionContext) { console.log('HumanAgent MCP extension activated!'); + // Initialize telemetry service + telemetryService = new TelemetryService(context); + await telemetryService.trackExtensionActivated(); + // Determine port based on extension mode (dev vs production) SERVER_PORT = context.extensionMode === vscode.ExtensionMode.Development ? 3738 : 3737; console.log(`Using port ${SERVER_PORT} (${context.extensionMode === vscode.ExtensionMode.Development ? 'development' : 'production'} mode);`); @@ -213,7 +219,7 @@ export async function activate(context: vscode.ExtensionContext) { }); // Initialize Chat Webview Provider (no internal server dependency) - const chatWebviewProvider = new ChatWebviewProvider(context.extensionUri, null, mcpConfigManager, workspaceSessionId, context, mcpProvider, SERVER_PORT); + const chatWebviewProvider = new ChatWebviewProvider(context.extensionUri, null, mcpConfigManager, workspaceSessionId, context, mcpProvider, SERVER_PORT, telemetryService); context.subscriptions.push( vscode.window.registerWebviewViewProvider(ChatWebviewProvider.viewType, chatWebviewProvider) ); @@ -223,6 +229,8 @@ export async function activate(context: vscode.ExtensionContext) { // Register Commands const openChatCommand = vscode.commands.registerCommand('humanagent-mcp.openChat', () => { + // Track chat opened from command palette + telemetryService.trackChatOpened('command_palette'); // Focus the chat webview vscode.commands.executeCommand('humanagent-mcp.chatView.focus'); }); @@ -576,6 +584,7 @@ async function registerSessionWithStandaloneServer(sessionId: string, forceRereg } } catch (error) { console.error(`HumanAgent MCP: Error registering session ${sessionId}, retrying in 3 seconds...`, error); + telemetryService.trackError('connection_error', error instanceof Error ? error.message : String(error)); // Wait 3 seconds and try once more await new Promise(resolve => setTimeout(resolve, 3000)); @@ -687,6 +696,11 @@ async function ensureServerAccessibleAndRegister(sessionId: string, configType: } export async function deactivate() { + // Track deactivation event + if (telemetryService) { + await telemetryService.trackExtensionDeactivated(); + } + if (workspaceSessionId) { // Unregister from standalone server await unregisterSessionWithStandaloneServer(workspaceSessionId); diff --git a/src/telemetry/telemetryService.ts b/src/telemetry/telemetryService.ts new file mode 100644 index 0000000..78be07b --- /dev/null +++ b/src/telemetry/telemetryService.ts @@ -0,0 +1,159 @@ +import * as vscode from 'vscode'; +import * as crypto from 'crypto'; + +/** + * Telemetry service for tracking anonymous usage metrics via Google Analytics 4 + */ +export class TelemetryService { + private readonly GA_MEASUREMENT_ID = 'G-87BY4Y6NMK'; + private readonly GA_API_SECRET = '_5nnRhGLTdKkllpkf88wsA'; + private readonly GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect'; + + private clientId: string; + private installDate: string; + private context: vscode.ExtensionContext; + + constructor(context: vscode.ExtensionContext) { + this.context = context; + + // Get or create persistent client ID + let storedClientId = context.globalState.get('telemetry_client_id'); + if (!storedClientId) { + storedClientId = crypto.randomUUID(); + context.globalState.update('telemetry_client_id', storedClientId); + + // Store install date + const installDate = new Date().toISOString(); + context.globalState.update('telemetry_install_date', installDate); + } + + this.clientId = storedClientId; + this.installDate = context.globalState.get('telemetry_install_date') || new Date().toISOString(); + } + + /** + * Check if telemetry is enabled (respects VS Code's telemetry setting) + */ + private isTelemetryEnabled(): boolean { + return vscode.env.isTelemetryEnabled; + } + + /** + * Calculate days since installation + */ + private getDaysSinceInstall(): number { + const install = new Date(this.installDate); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - install.getTime()); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; + } + + /** + * Get common event parameters included in all events + */ + private getCommonParams(): Record { + const packageJson = this.context.extension.packageJSON; + return { + extension_version: packageJson.version, + vscode_version: vscode.version, + platform: process.platform, + install_date: this.installDate.split('T')[0], // Just the date part + days_since_install: this.getDaysSinceInstall() + }; + } + + /** + * Send an event to GA4 + */ + private async sendEvent(eventName: string, eventParams: Record = {}): Promise { + if (!this.isTelemetryEnabled()) { + return; // Respect user's privacy settings + } + + try { + const payload = { + client_id: this.clientId, + events: [{ + name: eventName, + params: { + ...this.getCommonParams(), + ...eventParams + } + }] + }; + + const url = `${this.GA_ENDPOINT}?measurement_id=${this.GA_MEASUREMENT_ID}&api_secret=${this.GA_API_SECRET}`; + + await fetch(url, { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + // Silently fail - don't interrupt user experience for telemetry failures + console.error('Telemetry error:', error); + } + } + + // Extension lifecycle events + async trackExtensionActivated(): Promise { + await this.sendEvent('extension_activated'); + } + + async trackExtensionDeactivated(): Promise { + await this.sendEvent('extension_deactivated'); + } + + // Chat events + async trackChatOpened(source: 'tree_view' | 'command_palette' | 'other'): Promise { + await this.sendEvent('chat_opened', { source }); + } + + async trackMessageSent(messageLength: number, sessionId: string): Promise { + await this.sendEvent('message_sent', { + message_length: messageLength, + session_id: sessionId + }); + } + + async trackMessageReceived(messageLength: number, sessionId: string): Promise { + await this.sendEvent('message_received', { + message_length: messageLength, + session_id: sessionId + }); + } + + // MCP tool events + async trackToolCalled(toolName: string, sessionId: string): Promise { + await this.sendEvent('tool_called', { + tool_name: toolName, + session_id: sessionId + }); + } + + // Error events + async trackError(errorType: 'server_error' | 'connection_error' | 'other', errorMessage: string): Promise { + // Only send error type and sanitized message (no sensitive data) + const sanitizedMessage = errorMessage.substring(0, 100); // Truncate + await this.sendEvent('error_occurred', { + error_type: errorType, + error_message: sanitizedMessage + }); + } + + // Session events + async trackSessionStarted(sessionId: string): Promise { + await this.sendEvent('session_started', { session_id: sessionId }); + } + + async trackSessionEnded(sessionId: string, messageCount: number, durationMs: number): Promise { + await this.sendEvent('session_ended', { + session_id: sessionId, + message_count: messageCount, + duration_seconds: Math.floor(durationMs / 1000) + }); + } +} diff --git a/src/webview/chatWebviewProvider.ts b/src/webview/chatWebviewProvider.ts index b072798..f8abdcf 100644 --- a/src/webview/chatWebviewProvider.ts +++ b/src/webview/chatWebviewProvider.ts @@ -5,6 +5,7 @@ import { McpServer } from '../mcp/server'; import { ChatMessage } from '../mcp/types'; import { McpConfigManager } from '../mcp/mcpConfigManager'; import { AudioNotification } from '../audio/audioNotification'; +import { TelemetryService } from '../telemetry/telemetryService'; export class ChatWebviewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'humanagent-mcp.chatView'; @@ -28,7 +29,8 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider { private readonly workspaceSessionId?: string, private readonly context?: vscode.ExtensionContext, private readonly mcpProvider?: any, - private readonly port: number = 3737 + private readonly port: number = 3737, + private readonly telemetryService?: TelemetryService ) { this.mcpServer = mcpServer; this.mcpConfigManager = mcpConfigManager; @@ -71,6 +73,11 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider { // Combine context and message if context exists const fullMessage = context ? `${context}\n\n${message}` : message; + // Track message received + if (this.telemetryService && this.workspaceSessionId) { + this.telemetryService.trackMessageReceived(fullMessage.length, this.workspaceSessionId); + } + // Add AI message to chat const aiMessage: ChatMessage = { id: Date.now().toString(), @@ -136,6 +143,11 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider { ] }; + // Track chat opened from tree view + if (this.telemetryService) { + this.telemetryService.trackChatOpened('tree_view'); + } + // Load conversation history from centralized chat manager this.loadConversationHistory(); @@ -171,6 +183,11 @@ export class ChatWebviewProvider implements vscode.WebviewViewProvider { try { console.log('ChatWebviewProvider: Sending human response:', content, images ? `with ${images.length} images` : ''); + // Track message sent + if (this.telemetryService && this.workspaceSessionId) { + this.telemetryService.trackMessageSent(content.length, this.workspaceSessionId); + } + // Don't add to local messages array - let server handle storage and SSE handle updates // Send response back to standalone MCP server via HTTP