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
This commit is contained in:
B Harper
2025-11-20 14:34:50 +11:00
parent 123bed4a29
commit c5b1267686
5 changed files with 225 additions and 3 deletions
+32
View File
@@ -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
+1 -1
View File
@@ -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": {
+15 -1
View File
@@ -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);
+159
View File
@@ -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<string>('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<string>('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<string, any> {
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<string, any> = {}): Promise<void> {
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<void> {
await this.sendEvent('extension_activated');
}
async trackExtensionDeactivated(): Promise<void> {
await this.sendEvent('extension_deactivated');
}
// Chat events
async trackChatOpened(source: 'tree_view' | 'command_palette' | 'other'): Promise<void> {
await this.sendEvent('chat_opened', { source });
}
async trackMessageSent(messageLength: number, sessionId: string): Promise<void> {
await this.sendEvent('message_sent', {
message_length: messageLength,
session_id: sessionId
});
}
async trackMessageReceived(messageLength: number, sessionId: string): Promise<void> {
await this.sendEvent('message_received', {
message_length: messageLength,
session_id: sessionId
});
}
// MCP tool events
async trackToolCalled(toolName: string, sessionId: string): Promise<void> {
await this.sendEvent('tool_called', {
tool_name: toolName,
session_id: sessionId
});
}
// Error events
async trackError(errorType: 'server_error' | 'connection_error' | 'other', errorMessage: string): Promise<void> {
// 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<void> {
await this.sendEvent('session_started', { session_id: sessionId });
}
async trackSessionEnded(sessionId: string, messageCount: number, durationMs: number): Promise<void> {
await this.sendEvent('session_ended', {
session_id: sessionId,
message_count: messageCount,
duration_seconds: Math.floor(durationMs / 1000)
});
}
}
+18 -1
View File
@@ -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