/ bridge / debugUtils.ts
debugUtils.ts
  1  import {
  2    type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  3    logEvent,
  4  } from '../services/analytics/index.js'
  5  import { logForDebugging } from '../utils/debug.js'
  6  import { errorMessage } from '../utils/errors.js'
  7  import { jsonStringify } from '../utils/slowOperations.js'
  8  
  9  const DEBUG_MSG_LIMIT = 2000
 10  
 11  const SECRET_FIELD_NAMES = [
 12    'session_ingress_token',
 13    'environment_secret',
 14    'access_token',
 15    'secret',
 16    'token',
 17  ]
 18  
 19  const SECRET_PATTERN = new RegExp(
 20    `"(${SECRET_FIELD_NAMES.join('|')})"\\s*:\\s*"([^"]*)"`,
 21    'g',
 22  )
 23  
 24  const REDACT_MIN_LENGTH = 16
 25  
 26  export function redactSecrets(s: string): string {
 27    return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => {
 28      if (value.length < REDACT_MIN_LENGTH) {
 29        return `"${field}":"[REDACTED]"`
 30      }
 31      const redacted = `${value.slice(0, 8)}...${value.slice(-4)}`
 32      return `"${field}":"${redacted}"`
 33    })
 34  }
 35  
 36  /** Truncate a string for debug logging, collapsing newlines. */
 37  export function debugTruncate(s: string): string {
 38    const flat = s.replace(/\n/g, '\\n')
 39    if (flat.length <= DEBUG_MSG_LIMIT) {
 40      return flat
 41    }
 42    return flat.slice(0, DEBUG_MSG_LIMIT) + `... (${flat.length} chars)`
 43  }
 44  
 45  /** Truncate a JSON-serializable value for debug logging. */
 46  export function debugBody(data: unknown): string {
 47    const raw = typeof data === 'string' ? data : jsonStringify(data)
 48    const s = redactSecrets(raw)
 49    if (s.length <= DEBUG_MSG_LIMIT) {
 50      return s
 51    }
 52    return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)`
 53  }
 54  
 55  /**
 56   * Extract a descriptive error message from an axios error (or any error).
 57   * For HTTP errors, appends the server's response body message if available,
 58   * since axios's default message only includes the status code.
 59   */
 60  export function describeAxiosError(err: unknown): string {
 61    const msg = errorMessage(err)
 62    if (err && typeof err === 'object' && 'response' in err) {
 63      const response = (err as { response?: { data?: unknown } }).response
 64      if (response?.data && typeof response.data === 'object') {
 65        const data = response.data as Record<string, unknown>
 66        const detail =
 67          typeof data.message === 'string'
 68            ? data.message
 69            : typeof data.error === 'object' &&
 70                data.error &&
 71                'message' in data.error &&
 72                typeof (data.error as Record<string, unknown>).message ===
 73                  'string'
 74              ? (data.error as Record<string, unknown>).message
 75              : undefined
 76        if (detail) {
 77          return `${msg}: ${detail}`
 78        }
 79      }
 80    }
 81    return msg
 82  }
 83  
 84  /**
 85   * Extract the HTTP status code from an axios error, if present.
 86   * Returns undefined for non-HTTP errors (e.g. network failures).
 87   */
 88  export function extractHttpStatus(err: unknown): number | undefined {
 89    if (
 90      err &&
 91      typeof err === 'object' &&
 92      'response' in err &&
 93      (err as { response?: { status?: unknown } }).response &&
 94      typeof (err as { response: { status?: unknown } }).response.status ===
 95        'number'
 96    ) {
 97      return (err as { response: { status: number } }).response.status
 98    }
 99    return undefined
100  }
101  
102  /**
103   * Pull a human-readable message out of an API error response body.
104   * Checks `data.message` first, then `data.error.message`.
105   */
106  export function extractErrorDetail(data: unknown): string | undefined {
107    if (!data || typeof data !== 'object') return undefined
108    if ('message' in data && typeof data.message === 'string') {
109      return data.message
110    }
111    if (
112      'error' in data &&
113      data.error !== null &&
114      typeof data.error === 'object' &&
115      'message' in data.error &&
116      typeof data.error.message === 'string'
117    ) {
118      return data.error.message
119    }
120    return undefined
121  }
122  
123  /**
124   * Log a bridge init skip — debug message + `tengu_bridge_repl_skipped`
125   * analytics event. Centralizes the event name and the AnalyticsMetadata
126   * cast so call sites don't each repeat the 5-line boilerplate.
127   */
128  export function logBridgeSkip(
129    reason: string,
130    debugMsg?: string,
131    v2?: boolean,
132  ): void {
133    if (debugMsg) {
134      logForDebugging(debugMsg)
135    }
136    logEvent('tengu_bridge_repl_skipped', {
137      reason:
138        reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
139      ...(v2 !== undefined && { v2 }),
140    })
141  }