Files
claude-task-master/scripts/modules/telemetry-submission.js
Eyal Toledano 4e9d58a1b0 feat(config): Restructure .taskmasterconfig and enhance gateway integration
Config Structure Changes and Gateway Integration

## Configuration Structure Changes
- Restructured .taskmasterconfig to use 'account' section for user settings
- Moved userId, userEmail, mode, telemetryEnabled from global to account section
- API keys remain isolated in .env file (not accessible to AI)
- Enhanced getUserId() to always return value, never null (sets default '1234567890')

## Gateway Integration Enhancements
- Updated registerUserWithGateway() to accept both email and userId parameters
- Enhanced /auth/init endpoint integration for existing user validation
- API key updates automatically written to .env during registration process
- Improved user identification and validation flow

## Code Updates for New Structure
- Fixed config-manager.js getter functions for account section access
- Updated user-management.js to use config.account.userId/mode
- Modified telemetry-submission.js to read from account section
- Added getTelemetryEnabled() function with proper account section access
- Enhanced telemetry configuration reading with new structure

## Comprehensive Test Updates
- Updated integration tests (init-config.test.js) for new config structure
- Fixed unit tests (config-manager.test.js) with updated default config
- Updated telemetry tests (telemetry-submission.test.js) for account structure
- Added missing getTelemetryEnabled mock to ai-services-unified.test.js
- Fixed all test expectations to use config.account.* instead of config.global.*
- Removed references to deprecated config.subscription object

## Configuration Access Consistency
- Standardized configuration access patterns across entire codebase
- Clean separation: user settings in account, API keys in .env, models/global in respective sections
- All tests passing with new configuration structure
- Maintained backward compatibility during transition

Changes support enhanced telemetry system with proper user management and gateway integration while maintaining security through API key isolation.
2025-05-30 18:53:16 -04:00

242 lines
7.4 KiB
JavaScript

/**
* Telemetry Submission Service
* Handles sending telemetry data to remote gateway endpoint
*/
import { z } from "zod";
import { getConfig } from "./config-manager.js";
import { getTelemetryEnabled } from "./config-manager.js";
import { resolveEnvVariable } from "./utils.js";
// Telemetry data validation schema
const TelemetryDataSchema = z.object({
timestamp: z.string().datetime(),
userId: z.string().min(1),
commandName: z.string().min(1),
modelUsed: z.string().optional(),
providerName: z.string().optional(),
inputTokens: z.number().optional(),
outputTokens: z.number().optional(),
totalTokens: z.number().optional(),
totalCost: z.number().optional(),
currency: z.string().optional(),
commandArgs: z.any().optional(),
fullOutput: z.any().optional(),
});
// Hardcoded configuration for TaskMaster telemetry gateway
const TASKMASTER_BASE_URL = "http://localhost:4444";
const TASKMASTER_TELEMETRY_ENDPOINT = `${TASKMASTER_BASE_URL}/api/v1/telemetry`;
const TASKMASTER_USER_REGISTRATION_ENDPOINT = `${TASKMASTER_BASE_URL}/auth/init`;
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1 second
/**
* Get telemetry configuration from environment variables only
* @returns {Object} Configuration object with apiKey, userId, and email
*/
function getTelemetryConfig() {
// Try environment variables first (includes .env file via resolveEnvVariable)
const envApiKey =
resolveEnvVariable("TASKMASTER_API_KEY") ||
resolveEnvVariable("GATEWAY_API_KEY") ||
resolveEnvVariable("TELEMETRY_API_KEY");
const envUserId =
resolveEnvVariable("TASKMASTER_USER_ID") ||
resolveEnvVariable("GATEWAY_USER_ID") ||
resolveEnvVariable("TELEMETRY_USER_ID");
const envEmail =
resolveEnvVariable("TASKMASTER_USER_EMAIL") ||
resolveEnvVariable("GATEWAY_USER_EMAIL") ||
resolveEnvVariable("TELEMETRY_USER_EMAIL");
// Get the config (which might contain userId)
const config = getConfig();
return {
apiKey: envApiKey || null, // API key should only come from environment
userId: envUserId || config?.account?.userId || null,
email: envEmail || null,
};
}
/**
* Register or lookup user with the TaskMaster telemetry gateway using /auth/init
* @param {string} email - User's email address
* @param {string} userId - User's ID
* @returns {Promise<{success: boolean, apiKey?: string, userId?: string, email?: string, isNewUser?: boolean, error?: string}>}
*/
export async function registerUserWithGateway(email = null, userId = null) {
try {
const requestBody = {};
if (email) requestBody.email = email;
if (userId) requestBody.userId = userId;
const response = await fetch(TASKMASTER_USER_REGISTRATION_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
return {
success: false,
error: `Gateway registration failed: ${response.status} ${response.statusText}`,
};
}
const result = await response.json();
// Handle the /auth/init response format
if (result.success && result.data) {
return {
success: true,
apiKey: result.data.token,
userId: result.data.userId,
email: email,
isNewUser: result.data.isNewUser,
};
} else {
return {
success: false,
error: result.error || result.message || "Unknown registration error",
};
}
} catch (error) {
return {
success: false,
error: `Gateway registration error: ${error.message}`,
};
}
}
/**
* Submits telemetry data to the remote gateway endpoint
* @param {Object} telemetryData - The telemetry data to submit
* @returns {Promise<Object>} - Result object with success status and details
*/
export async function submitTelemetryData(telemetryData) {
try {
// Check user opt-out preferences first
if (!getTelemetryEnabled()) {
return {
success: true,
skipped: true,
reason: "Telemetry disabled by user preference",
};
}
// Get telemetry configuration
const telemetryConfig = getTelemetryConfig();
if (
!telemetryConfig.apiKey ||
!telemetryConfig.userId ||
!telemetryConfig.email
) {
return {
success: false,
error:
"Telemetry configuration incomplete. Run 'task-master init' and select hosted gateway option, or manually set TASKMASTER_API_KEY, TASKMASTER_USER_ID, and TASKMASTER_USER_EMAIL environment variables",
};
}
// Validate telemetry data
try {
TelemetryDataSchema.parse(telemetryData);
} catch (validationError) {
return {
success: false,
error: `Telemetry data validation failed: ${validationError.message}`,
};
}
// Send FULL telemetry data to gateway (including commandArgs and fullOutput)
// Note: Sensitive data filtering is handled separately for user-facing responses
const completeTelemetryData = {
...telemetryData,
userId: telemetryConfig.userId, // Ensure correct userId
};
// Attempt submission with retry logic
let lastError;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const response = await fetch(TASKMASTER_TELEMETRY_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${telemetryConfig.apiKey}`, // Use Bearer token format
"X-User-Email": telemetryConfig.email, // Add required email header
},
body: JSON.stringify(completeTelemetryData),
});
if (response.ok) {
const result = await response.json();
return {
success: true,
id: result.id,
attempt,
};
} else {
// Handle HTTP error responses
const errorData = await response.json().catch(() => ({}));
const errorMessage = `HTTP ${response.status} ${response.statusText}`;
// Don't retry on certain status codes (rate limiting, auth errors, etc.)
if (
response.status === 429 ||
response.status === 401 ||
response.status === 403
) {
return {
success: false,
error: errorMessage,
statusCode: response.status,
};
}
// For other HTTP errors, continue retrying
lastError = new Error(errorMessage);
}
} catch (networkError) {
lastError = networkError;
}
// Wait before retry (exponential backoff)
if (attempt < MAX_RETRIES) {
await new Promise((resolve) =>
setTimeout(resolve, RETRY_DELAY * Math.pow(2, attempt - 1))
);
}
}
// All retries failed
return {
success: false,
error: lastError.message,
attempts: MAX_RETRIES,
};
} catch (error) {
// Graceful error handling - never throw
return {
success: false,
error: `Telemetry submission failed: ${error.message}`,
};
}
}
/**
* Submits telemetry data asynchronously without blocking execution
* @param {Object} telemetryData - The telemetry data to submit
*/
export function submitTelemetryDataAsync(telemetryData) {
// Fire and forget - don't block execution
submitTelemetryData(telemetryData).catch((error) => {
// Silently log errors without blocking
console.debug("Telemetry submission failed:", error);
});
}