Files
claude-task-master/scripts/modules/telemetry-submission.js
Eyal Toledano 2819be51d3 feat: Implement TaskMaster AI Gateway integration with enhanced UX
- Fix Zod schema conversion, update headers, add premium telemetry display, improve user auth flow, and standardize email fields

Functionally complete on this end, mostly polish around user experience and need to add in profile, upgrade/downgrade, etc.

But the AI commands are working off the gateway.
2025-06-01 19:37:12 -04:00

239 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 hardcoded service ID, user token, and config
* @returns {Object} Configuration object with serviceId, apiKey, userId, and email
*/
function getTelemetryConfig() {
// Get the config which contains userId and email
const config = getConfig();
// Hardcoded service ID for TaskMaster telemetry service
const hardcodedServiceId = "98fb3198-2dfc-42d1-af53-07b99e4f3bde";
// Get user's API token from .env (managed by user-management.js)
const userApiKey = resolveEnvVariable("TASKMASTER_API_KEY");
return {
serviceId: hardcodedServiceId, // Hardcoded service identifier
apiKey: userApiKey || null, // User's Bearer token from .env
userId: config?.account?.userId || null, // From config
email: config?.account?.email || null, // From config
};
}
/**
* 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, but hosted mode always sends telemetry
const config = getConfig();
const isHostedMode = config?.account?.mode === "hosted";
if (!isHostedMode && !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. Please ensure you have completed 'task-master init' to set up your user account.",
};
}
// 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",
"X-Service-ID": telemetryConfig.serviceId, // Hardcoded service ID
Authorization: `Bearer ${telemetryConfig.apiKey}`, // User's Bearer token
"X-User-Email": telemetryConfig.email, // User's email from config
},
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);
});
}