import { createHash } from 'crypto';
/**
* Sentry initialization and configuration for Task Master
* Provides error tracking and AI operation monitoring
*/
import * as Sentry from '@sentry/node';
import {
getAnonymousTelemetryEnabled,
setSuppressConfigWarnings
} from '../../scripts/modules/config-manager.js';
let isInitialized = false;
/**
* Create a privacy-safe hash of a project root path
* Uses SHA256 and truncates to 8 characters for grouping without exposing full paths
* @param {string} projectRoot - The project root path
* @returns {string|undefined} Short hash of the project root, or undefined if no path provided
*/
export function hashProjectRoot(projectRoot) {
if (!projectRoot) return undefined;
// Create SHA256 hash and take first 8 characters for grouping
return createHash('sha256').update(projectRoot).digest('hex').substring(0, 8);
}
/**
* Initialize Sentry with AI telemetry integration
* @param {object} options - Initialization options
* @param {string} [options.dsn] - Sentry DSN (defaults to env var SENTRY_DSN)
* @param {string} [options.environment] - Environment name (development, production, etc.)
* @param {number} [options.tracesSampleRate] - Traces sample rate (0.0 to 1.0)
* @param {boolean} [options.sendDefaultPii] - Whether to send PII data
* @param {object} [options.session] - MCP session for env resolution
* @param {string} [options.projectRoot] - Project root for .env file resolution
*/
export function initializeSentry(options = {}) {
// Avoid double initialization
if (isInitialized) {
return;
}
// Check if user has opted out of anonymous telemetry
// This applies to local storage users only
// Hamster users don't use local config (API storage), so this check doesn't affect them
// Suppress config warnings during this check to avoid noisy output at startup
setSuppressConfigWarnings(true);
try {
const telemetryEnabled = getAnonymousTelemetryEnabled(options.projectRoot);
if (!telemetryEnabled) {
console.log(
'✓ Anonymous telemetry disabled per user preference. ' +
'Set anonymousTelemetry: true in .taskmaster/config.json to re-enable.'
);
return;
}
} catch (error) {
// If there's an error checking telemetry preferences (e.g., config not available yet),
// default to enabled. This ensures telemetry works during initialization.
} finally {
setSuppressConfigWarnings(false);
}
// Use internal Sentry DSN for Task Master telemetry
// This is a public client-side DSN and is safe to hardcode
const dsn =
options.dsn ||
'https://ce8c03ca1dd0da5b9837c6ba1b3a0f9d@o4510099843776512.ingest.us.sentry.io/4510381945585664';
// DSN is always available, but check if user has opted out
if (!dsn) {
return;
}
try {
Sentry.init({
dsn,
environment: options.environment || process.env.NODE_ENV || 'production',
integrations: [
// Add the Vercel AI SDK integration for automatic AI operation tracking
Sentry.vercelAIIntegration({
recordInputs: true,
recordOutputs: true
}),
// Add Zod error tracking for better validation error reporting
Sentry.zodErrorsIntegration()
],
// Tracing must be enabled for AI monitoring to work
tracesSampleRate: options.tracesSampleRate ?? 1.0,
sendDefaultPii: options.sendDefaultPii ?? true,
// Enable debug mode with SENTRY_DEBUG=true env var
debug: process.env.SENTRY_DEBUG === 'true'
});
isInitialized = true;
if (process.env.SENTRY_DEBUG === 'true') {
console.log(` DSN: ${dsn.substring(0, 40)}...`);
console.log(
` Environment: ${options.environment || process.env.NODE_ENV || 'production'}`
);
console.log(` Traces Sample Rate: ${options.tracesSampleRate ?? 1.0}`);
}
} catch (error) {
console.error(`Failed to initialize telemetry: ${error.message}`);
}
}
/**
* Get the experimental telemetry configuration for AI SDK calls
* Only returns telemetry config if Sentry is initialized
* @param {string} [functionId] - Optional function identifier to help correlate spans with function calls
* @param {object} [metadata] - Optional metadata to include in telemetry spans
* @param {string} [metadata.command] - Command name (e.g., 'add-task', 'update-task')
* @param {string} [metadata.outputType] - Output type: 'cli' or 'mcp'
* @param {string} [metadata.tag] - Task tag being operated on
* @param {string} [metadata.taskId] - Specific task ID if applicable
* @param {string} [metadata.userId] - Hamster user ID if authenticated
* @param {string} [metadata.briefId] - Hamster brief ID if connected
* @param {string} [metadata.projectHash] - Privacy-safe hash of project root
* @returns {object|null} Telemetry configuration or null if Sentry not initialized
*/
export function getAITelemetryConfig(functionId, metadata = {}) {
if (!isInitialized) {
if (process.env.SENTRY_DEBUG === 'true') {
console.log('⚠️ Sentry not initialized, telemetry config not available');
}
return null;
}
const config = {
isEnabled: true,
recordInputs: true,
recordOutputs: true
};
// Add functionId if provided - helps correlate captured spans with function calls
if (functionId) {
config.functionId = functionId;
}
// Add custom metadata for better filtering and grouping in Sentry
// Only include defined metadata fields to avoid clutter
if (Object.keys(metadata).length > 0) {
config.metadata = {};
if (metadata.command) config.metadata.command = metadata.command;
if (metadata.outputType) config.metadata.outputType = metadata.outputType;
if (metadata.tag) config.metadata.tag = metadata.tag;
if (metadata.taskId) config.metadata.taskId = metadata.taskId;
if (metadata.userId) config.metadata.userId = metadata.userId;
if (metadata.briefId) config.metadata.briefId = metadata.briefId;
if (metadata.projectHash)
config.metadata.projectHash = metadata.projectHash;
}
if (process.env.SENTRY_DEBUG === 'true') {
console.log(
'📊 Sentry telemetry config created:',
JSON.stringify(config, null, 2)
);
}
return config;
}
/**
* Check if Sentry is initialized
* @returns {boolean} True if Sentry is initialized
*/
export function isSentryInitialized() {
return isInitialized;
}
/**
* Flush all pending Sentry events
* Critical for short-lived processes like CLI commands
* @param {number} [timeout=2000] - Maximum time to wait for events to flush (ms)
* @returns {Promise<boolean>} True if flush was successful
*/
export async function flushSentry(timeout = 2000) {
if (!isInitialized) {
return false;
}
try {
if (process.env.SENTRY_DEBUG === 'true') {
console.log('🔄 Flushing Sentry events...');
}
await Sentry.flush(timeout);
if (process.env.SENTRY_DEBUG === 'true') {
console.log('✓ Sentry events flushed successfully');
}
return true;
} catch (error) {
console.error(`Failed to flush Sentry events: ${error.message}`);
return false;
}
}
/**
* Capture an exception with Sentry
* @param {Error} error - The error to capture
* @param {object} [context] - Additional context data
*/
export function captureException(error, context = {}) {
if (!isInitialized) {
return;
}
Sentry.captureException(error, {
extra: context
});
}
/**
* Capture a message with Sentry
* @param {string} message - The message to capture
* @param {string} [level] - Severity level (fatal, error, warning, log, info, debug)
* @param {object} [context] - Additional context data
*/
export function captureMessage(message, level = 'info', context = {}) {
if (!isInitialized) {
return;
}
Sentry.captureMessage(message, {
level,
extra: context
});
}
/**
* Set user context for Sentry events
* @param {object} user - User information
* @param {string} [user.id] - User ID
* @param {string} [user.email] - User email
* @param {string} [user.username] - Username
*/
export function setUser(user) {
if (!isInitialized) {
return;
}
Sentry.setUser(user);
}
/**
* Add tags to Sentry events
* @param {object} tags - Tags to add
*/
export function setTags(tags) {
if (!isInitialized) {
return;
}
Sentry.setTags(tags);
}
/**
* Reset Sentry initialization state (useful for testing)
* @private
*/
export function _resetSentry() {
isInitialized = false;
}