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.
242 lines
7.4 KiB
JavaScript
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);
|
|
});
|
|
}
|