/ src / utils / warningHandler.ts
warningHandler.ts
  1  import { posix, win32 } from 'path'
  2  import {
  3    type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  4    logEvent,
  5  } from 'src/services/analytics/index.js'
  6  import { logForDebugging } from './debug.js'
  7  import { isEnvTruthy } from './envUtils.js'
  8  import { getPlatform } from './platform.js'
  9  
 10  // Track warnings to avoid spam — bounded to prevent unbounded memory growth
 11  export const MAX_WARNING_KEYS = 1000
 12  const warningCounts = new Map<string, number>()
 13  
 14  // Check if running from a build directory (development mode)
 15  // This is a sync version of the logic in getCurrentInstallationType()
 16  function isRunningFromBuildDirectory(): boolean {
 17    let invokedPath = process.argv[1] || ''
 18    let execPath = process.execPath || process.argv[0] || ''
 19  
 20    // On Windows, convert backslashes to forward slashes for consistent path matching
 21    if (getPlatform() === 'windows') {
 22      invokedPath = invokedPath.split(win32.sep).join(posix.sep)
 23      execPath = execPath.split(win32.sep).join(posix.sep)
 24    }
 25  
 26    const pathsToCheck = [invokedPath, execPath]
 27    const buildDirs = [
 28      '/build-ant/',
 29      '/build-external/',
 30      '/build-external-native/',
 31      '/build-ant-native/',
 32    ]
 33  
 34    return pathsToCheck.some(path => buildDirs.some(dir => path.includes(dir)))
 35  }
 36  
 37  // Warnings we know about and want to suppress from users
 38  const INTERNAL_WARNINGS = [
 39    /MaxListenersExceededWarning.*AbortSignal/,
 40    /MaxListenersExceededWarning.*EventTarget/,
 41  ]
 42  
 43  function isInternalWarning(warning: Error): boolean {
 44    const warningStr = `${warning.name}: ${warning.message}`
 45    return INTERNAL_WARNINGS.some(pattern => pattern.test(warningStr))
 46  }
 47  
 48  // Store reference to our warning handler so we can detect if it's already installed
 49  let warningHandler: ((warning: Error) => void) | null = null
 50  
 51  // For testing only - allows resetting the warning handler state
 52  export function resetWarningHandler(): void {
 53    if (warningHandler) {
 54      process.removeListener('warning', warningHandler)
 55    }
 56    warningHandler = null
 57    warningCounts.clear()
 58  }
 59  
 60  export function initializeWarningHandler(): void {
 61    // Only set up handler once - check if our handler is already installed
 62    const currentListeners = process.listeners('warning')
 63    if (warningHandler && currentListeners.includes(warningHandler)) {
 64      return
 65    }
 66  
 67    // For external users, remove default Node.js handler to suppress stderr output
 68    // For internal users, only keep default warnings for development builds
 69    // Check development mode directly to avoid async call in init
 70    // This preserves the same logic as getCurrentInstallationType() without async
 71    const isDevelopment =
 72      process.env.NODE_ENV === 'development' || isRunningFromBuildDirectory()
 73    if (!isDevelopment) {
 74      process.removeAllListeners('warning')
 75    }
 76  
 77    // Create and store our warning handler
 78    warningHandler = (warning: Error) => {
 79      try {
 80        const warningKey = `${warning.name}: ${warning.message.slice(0, 50)}`
 81        const count = warningCounts.get(warningKey) || 0
 82  
 83        // Bound the map to prevent unbounded memory growth from unique warning keys.
 84        // Once the cap is reached, new unique keys are not tracked — their
 85        // occurrence_count will always be reported as 1 in analytics.
 86        if (
 87          warningCounts.has(warningKey) ||
 88          warningCounts.size < MAX_WARNING_KEYS
 89        ) {
 90          warningCounts.set(warningKey, count + 1)
 91        }
 92  
 93        const isInternal = isInternalWarning(warning)
 94  
 95        // Always log to Statsig for monitoring
 96        // Include full details for ant users only, since they may contain code or filepaths
 97        logEvent('tengu_node_warning', {
 98          is_internal: isInternal ? 1 : 0,
 99          occurrence_count: count + 1,
100          classname:
101            warning.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
102          ...(process.env.USER_TYPE === 'ant' && {
103            message:
104              warning.message as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
105          }),
106        })
107  
108        // In debug mode, show all warnings with context
109        if (isEnvTruthy(process.env.CLAUDE_DEBUG)) {
110          const prefix = isInternal ? '[Internal Warning]' : '[Warning]'
111          logForDebugging(`${prefix} ${warning.toString()}`, { level: 'warn' })
112        }
113        // Hide all warnings from users - they are only logged to Statsig for monitoring
114      } catch {
115        // Fail silently - we don't want the warning handler to cause issues
116      }
117    }
118  
119    // Install the warning handler
120    process.on('warning', warningHandler)
121  }