debugUtils.ts 4.1 KB

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