fix: suppress config warnings during Sentry init and API mode detection (#1482)

This commit is contained in:
Ralph Khreish
2025-12-03 23:08:49 +01:00
committed by GitHub
parent 8674007cce
commit 0e7c068e86
9 changed files with 107 additions and 38 deletions

View File

@@ -9,7 +9,9 @@
*/ */
import { join } from 'node:path'; import { join } from 'node:path';
import { findProjectRoot } from '@tm/core'; import { AuthManager, findProjectRoot } from '@tm/core';
import { setSuppressConfigWarnings } from './modules/config-manager.js';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { initializeSentry } from '../src/telemetry/sentry.js'; import { initializeSentry } from '../src/telemetry/sentry.js';
@@ -35,6 +37,19 @@ if (process.env.DEBUG === '1') {
console.error('DEBUG - dev.js received args:', process.argv.slice(2)); console.error('DEBUG - dev.js received args:', process.argv.slice(2));
} }
// Suppress config warnings if user is authenticated (API mode)
// When authenticated, we don't need local config - everything is remote
try {
const authManager = AuthManager.getInstance();
const hasValidSession = await authManager.hasValidSession();
if (hasValidSession) {
setSuppressConfigWarnings(true);
}
} catch {
setSuppressConfigWarnings(false);
// Auth check failed, continue without suppressing
}
// Use dynamic import to ensure dotenv.config() runs before module-level code executes // Use dynamic import to ensure dotenv.config() runs before module-level code executes
const { runCLI } = await import('./modules/commands.js'); const { runCLI } = await import('./modules/commands.js');

View File

@@ -72,7 +72,8 @@ import {
getDebugFlag, getDebugFlag,
getDefaultNumTasks, getDefaultNumTasks,
isApiKeySet, isApiKeySet,
isConfigFilePresent isConfigFilePresent,
setSuppressConfigWarnings
} from './config-manager.js'; } from './config-manager.js';
import { import {
@@ -161,13 +162,17 @@ function isConnectedToHamster() {
} }
// Fallback: Check if storage type is 'api' (user selected Hamster during init) // Fallback: Check if storage type is 'api' (user selected Hamster during init)
// Suppress warnings during this check since we're detecting API mode
setSuppressConfigWarnings(true);
try { try {
const config = getConfig(); const config = getConfig(null, false, { storageType: 'api' });
if (config?.storage?.type === 'api') { if (config?.storage?.type === 'api') {
return true; return true;
} }
} catch { } catch {
// Config check failed, continue // Config check failed, continue
} finally {
setSuppressConfigWarnings(false);
} }
return false; return false;

View File

@@ -72,6 +72,23 @@ const DEFAULTS = {
let loadedConfig = null; let loadedConfig = null;
let loadedConfigRoot = null; // Track which root loaded the config let loadedConfigRoot = null; // Track which root loaded the config
/**
* Suppress config file warnings (useful during API mode detection)
* Uses global object so it can be shared across modules without circular deps
* @param {boolean} suppress - Whether to suppress warnings
*/
export function setSuppressConfigWarnings(suppress) {
global._tmSuppressConfigWarnings = suppress;
}
/**
* Check if config warnings are currently suppressed
* @returns {boolean}
*/
export function isConfigWarningSuppressed() {
return global._tmSuppressConfigWarnings === true;
}
// Custom Error for configuration issues // Custom Error for configuration issues
class ConfigurationError extends Error { class ConfigurationError extends Error {
constructor(message) { constructor(message) {
@@ -80,9 +97,10 @@ class ConfigurationError extends Error {
} }
} }
function _loadAndValidateConfig(explicitRoot = null) { function _loadAndValidateConfig(explicitRoot = null, options = {}) {
const defaults = DEFAULTS; // Use the defined defaults const defaults = DEFAULTS; // Use the defined defaults
let rootToUse = explicitRoot; let rootToUse = explicitRoot;
const { storageType } = options;
let configSource = explicitRoot let configSource = explicitRoot
? `explicit root (${explicitRoot})` ? `explicit root (${explicitRoot})`
: 'defaults (no root provided yet)'; : 'defaults (no root provided yet)';
@@ -114,7 +132,7 @@ function _loadAndValidateConfig(explicitRoot = null) {
if (hasProjectMarkers) { if (hasProjectMarkers) {
// Only try to find config if we have project markers // Only try to find config if we have project markers
// This prevents the repeated warnings during init // This prevents the repeated warnings during init
configPath = findConfigPath(null, { projectRoot: rootToUse }); configPath = findConfigPath(null, { projectRoot: rootToUse, storageType });
} }
if (configPath) { if (configPath) {
@@ -203,29 +221,36 @@ function _loadAndValidateConfig(explicitRoot = null) {
} }
} else { } else {
// Config file doesn't exist at the determined rootToUse. // Config file doesn't exist at the determined rootToUse.
if (explicitRoot) { // Skip warnings if:
// Only warn if an explicit root was *expected*. // 1. Global suppress flag is set (during API mode detection)
console.warn( // 2. storageType is explicitly 'api' (remote storage mode - no local config expected)
chalk.yellow( const shouldWarn = !isConfigWarningSuppressed() && storageType !== 'api';
`Warning: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
)
);
} else {
// Don't warn about missing config during initialization
// Only warn if this looks like an existing project (has .taskmaster dir or legacy config marker)
const hasTaskmasterDir = fs.existsSync(
path.join(rootToUse, TASKMASTER_DIR)
);
const hasLegacyMarker = fs.existsSync(
path.join(rootToUse, LEGACY_CONFIG_FILE)
);
if (hasTaskmasterDir || hasLegacyMarker) { if (shouldWarn) {
if (explicitRoot) {
// Warn about explicit root not having config
console.warn( console.warn(
chalk.yellow( chalk.yellow(
`Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.` `Warning: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
) )
); );
} else {
// Don't warn about missing config during initialization
// Only warn if this looks like an existing project (has .taskmaster dir or legacy config marker)
const hasTaskmasterDir = fs.existsSync(
path.join(rootToUse, TASKMASTER_DIR)
);
const hasLegacyMarker = fs.existsSync(
path.join(rootToUse, LEGACY_CONFIG_FILE)
);
if (hasTaskmasterDir || hasLegacyMarker) {
console.warn(
chalk.yellow(
`Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.`
)
);
}
} }
} }
// Keep config as defaults // Keep config as defaults
@@ -241,9 +266,11 @@ function _loadAndValidateConfig(explicitRoot = null) {
* Handles MCP initialization context gracefully. * Handles MCP initialization context gracefully.
* @param {string|null} explicitRoot - Optional explicit path to the project root. * @param {string|null} explicitRoot - Optional explicit path to the project root.
* @param {boolean} forceReload - Force reloading the config file. * @param {boolean} forceReload - Force reloading the config file.
* @param {object} options - Optional configuration options.
* @param {'api'|'file'|'auto'} [options.storageType] - Storage type to suppress warnings for API mode.
* @returns {object} The loaded configuration object. * @returns {object} The loaded configuration object.
*/ */
function getConfig(explicitRoot = null, forceReload = false) { function getConfig(explicitRoot = null, forceReload = false, options = {}) {
// Determine if a reload is necessary // Determine if a reload is necessary
const needsLoad = const needsLoad =
!loadedConfig || !loadedConfig ||
@@ -251,7 +278,7 @@ function getConfig(explicitRoot = null, forceReload = false) {
(explicitRoot && explicitRoot !== loadedConfigRoot); (explicitRoot && explicitRoot !== loadedConfigRoot);
if (needsLoad) { if (needsLoad) {
const newConfig = _loadAndValidateConfig(explicitRoot); // _load handles null explicitRoot const newConfig = _loadAndValidateConfig(explicitRoot, options); // _load handles null explicitRoot
// Only update the global cache if loading was forced or if an explicit root // Only update the global cache if loading was forced or if an explicit root
// was provided (meaning we attempted to load a specific project's config). // was provided (meaning we attempted to load a specific project's config).

View File

@@ -4,8 +4,10 @@ import { createHash } from 'crypto';
* Provides error tracking and AI operation monitoring * Provides error tracking and AI operation monitoring
*/ */
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { getAnonymousTelemetryEnabled } from '../../scripts/modules/config-manager.js'; import {
import { resolveEnvVariable } from '../../scripts/modules/utils.js'; getAnonymousTelemetryEnabled,
setSuppressConfigWarnings
} from '../../scripts/modules/config-manager.js';
let isInitialized = false; let isInitialized = false;
@@ -41,6 +43,8 @@ export function initializeSentry(options = {}) {
// Check if user has opted out of anonymous telemetry // Check if user has opted out of anonymous telemetry
// This applies to local storage users only // This applies to local storage users only
// Hamster users don't use local config (API storage), so this check doesn't affect them // 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 { try {
const telemetryEnabled = getAnonymousTelemetryEnabled(options.projectRoot); const telemetryEnabled = getAnonymousTelemetryEnabled(options.projectRoot);
@@ -54,6 +58,8 @@ export function initializeSentry(options = {}) {
} catch (error) { } catch (error) {
// If there's an error checking telemetry preferences (e.g., config not available yet), // If there's an error checking telemetry preferences (e.g., config not available yet),
// default to enabled. This ensures telemetry works during initialization. // default to enabled. This ensures telemetry works during initialization.
} finally {
setSuppressConfigWarnings(false);
} }
// Use internal Sentry DSN for Task Master telemetry // Use internal Sentry DSN for Task Master telemetry

View File

@@ -23,6 +23,7 @@ import {
TASKMASTER_TASKS_FILE TASKMASTER_TASKS_FILE
} from '../constants/paths.js'; } from '../constants/paths.js';
import { getLoggerOrDefault } from './logger-utils.js'; import { getLoggerOrDefault } from './logger-utils.js';
import { isConfigWarningSuppressed } from '../../scripts/modules/config-manager.js';
/** /**
* Normalize project root to ensure it doesn't end with .taskmaster * Normalize project root to ensure it doesn't end with .taskmaster
@@ -453,15 +454,22 @@ export function findConfigPath(explicitPath = null, args = null, log = null) {
} }
// Only warn once per command execution to prevent spam during init // Only warn once per command execution to prevent spam during init
const warningKey = `config_warning_${projectRoot}`; // Skip warning if:
// Global suppress flag is set (during API mode detection)
const shouldSkipWarning =
isConfigWarningSuppressed() || args?.storageType === 'api';
if (!global._tmConfigWarningsThisRun) { if (!shouldSkipWarning) {
global._tmConfigWarningsThisRun = new Set(); const warningKey = `config_warning_${projectRoot}`;
}
if (!global._tmConfigWarningsThisRun.has(warningKey)) { if (!global._tmConfigWarningsThisRun) {
global._tmConfigWarningsThisRun.add(warningKey); global._tmConfigWarningsThisRun = new Set();
logger.warn?.(`No configuration file found in project: ${projectRoot}`); }
if (!global._tmConfigWarningsThisRun.has(warningKey)) {
global._tmConfigWarningsThisRun.add(warningKey);
logger.warn?.(`No configuration file found in project: ${projectRoot}`);
}
} }
return null; return null;

View File

@@ -63,7 +63,9 @@ jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
getDebugFlag: jest.fn(() => false), getDebugFlag: jest.fn(() => false),
getLogLevel: jest.fn(() => 'info'), getLogLevel: jest.fn(() => 'info'),
isProxyEnabled: jest.fn(() => false), isProxyEnabled: jest.fn(() => false),
getAnonymousTelemetryEnabled: jest.fn(() => true) getAnonymousTelemetryEnabled: jest.fn(() => true),
setSuppressConfigWarnings: jest.fn(),
isConfigWarningSuppressed: jest.fn(() => false)
})); }));
// Mock utils // Mock utils

View File

@@ -38,7 +38,9 @@ jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({ jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
isProxyEnabled: jest.fn(() => false), isProxyEnabled: jest.fn(() => false),
getAnonymousTelemetryEnabled: jest.fn(() => true) getAnonymousTelemetryEnabled: jest.fn(() => true),
setSuppressConfigWarnings: jest.fn(),
isConfigWarningSuppressed: jest.fn(() => false)
})); }));
// Import after mocking // Import after mocking

View File

@@ -17,7 +17,9 @@ jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({ jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
isProxyEnabled: jest.fn(() => false), isProxyEnabled: jest.fn(() => false),
getAnonymousTelemetryEnabled: jest.fn(() => true) getAnonymousTelemetryEnabled: jest.fn(() => true),
setSuppressConfigWarnings: jest.fn(),
isConfigWarningSuppressed: jest.fn(() => false)
})); }));
// Import after mocking // Import after mocking

View File

@@ -189,7 +189,9 @@ jest.unstable_mockModule(
getAllProviders: jest.fn(() => ['anthropic', 'openai', 'perplexity']), getAllProviders: jest.fn(() => ['anthropic', 'openai', 'perplexity']),
getVertexProjectId: jest.fn(() => undefined), getVertexProjectId: jest.fn(() => undefined),
getVertexLocation: jest.fn(() => undefined), getVertexLocation: jest.fn(() => undefined),
hasCodebaseAnalysis: jest.fn(() => false) hasCodebaseAnalysis: jest.fn(() => false),
setSuppressConfigWarnings: jest.fn(),
isConfigWarningSuppressed: jest.fn(() => false)
}) })
); );