upload-diagnostics.mjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. function getCauseField(cause, field) {
  2. if (!cause || !(field in cause)) return null
  3. const value = cause[field]
  4. return value === null || value === undefined ? null : String(value)
  5. }
  6. function normalizeTarget(target) {
  7. if (!target) {
  8. return null
  9. }
  10. if (typeof target === 'string') {
  11. return target
  12. }
  13. if (typeof target === 'object' && target !== null) {
  14. if ('pathname' in target && typeof target.pathname === 'string') {
  15. const search = 'search' in target && typeof target.search === 'string' ? target.search : ''
  16. return `${target.pathname}${search}`
  17. }
  18. if ('href' in target && typeof target.href === 'string') {
  19. return target.href
  20. }
  21. }
  22. return String(target)
  23. }
  24. function getCause(error) {
  25. if (!(error instanceof Error)) {
  26. return null
  27. }
  28. if (typeof error.cause === 'object' && error.cause !== null) {
  29. return error.cause
  30. }
  31. return null
  32. }
  33. function pickEnumerableFields(value) {
  34. if (!value || typeof value !== 'object') {
  35. return null
  36. }
  37. const entries = Object.entries(value)
  38. .filter(([, entryValue]) => {
  39. return entryValue === null || [ 'string', 'number', 'boolean' ].includes(typeof entryValue)
  40. })
  41. return entries.length > 0 ? Object.fromEntries(entries) : null
  42. }
  43. export function extractErrorDetails(error) {
  44. const normalizedError = error instanceof Error ? error : new Error(String(error))
  45. const cause = getCause(normalizedError)
  46. return {
  47. causeCode: getCauseField(cause, 'code'),
  48. causeErrno: getCauseField(cause, 'errno'),
  49. causeHostname: getCauseField(cause, 'hostname'),
  50. causeMessage: getCauseField(cause, 'message'),
  51. causeSyscall: getCauseField(cause, 'syscall'),
  52. errorMessage: normalizedError.message,
  53. errorName: normalizedError.name || 'Error',
  54. rawCause: pickEnumerableFields(cause),
  55. stack: normalizedError.stack || null,
  56. }
  57. }
  58. export function buildDiagnostic({
  59. attempt,
  60. error,
  61. fileIndex = null,
  62. fileName = null,
  63. httpStatus = null,
  64. maxAttempts,
  65. method,
  66. progressSnapshot = null,
  67. retryable,
  68. stage,
  69. target = null,
  70. }) {
  71. const errorDetails = extractErrorDetails(error)
  72. return {
  73. attempt,
  74. causeCode: errorDetails.causeCode,
  75. causeErrno: errorDetails.causeErrno,
  76. causeHostname: errorDetails.causeHostname,
  77. causeMessage: errorDetails.causeMessage,
  78. causeSyscall: errorDetails.causeSyscall,
  79. currentFileBytes: progressSnapshot?.currentFileBytes ?? null,
  80. errorMessage: errorDetails.errorMessage,
  81. errorName: errorDetails.errorName,
  82. fileIndex,
  83. fileName,
  84. httpStatus,
  85. maxAttempts,
  86. method,
  87. retryable: Boolean(retryable),
  88. stage,
  89. target: normalizeTarget(target),
  90. totalFiles: progressSnapshot?.totalFiles ?? null,
  91. totalUploadedBytes: progressSnapshot?.totalUploadedBytes ?? null,
  92. }
  93. }
  94. export function formatDiagnosticSummary(diagnostic) {
  95. const subject = diagnostic.fileName || diagnostic.target || diagnostic.stage
  96. const details = [ `attempt ${diagnostic.attempt}/${diagnostic.maxAttempts}` ]
  97. if (diagnostic.fileIndex != null && diagnostic.totalFiles) {
  98. details.push(`file ${diagnostic.fileIndex}/${diagnostic.totalFiles}`)
  99. }
  100. if (diagnostic.httpStatus) {
  101. details.push(`status=${diagnostic.httpStatus}`)
  102. }
  103. if (diagnostic.causeCode) {
  104. details.push(`cause=${diagnostic.causeCode}`)
  105. }
  106. if (diagnostic.causeMessage) {
  107. details.push(`message=${diagnostic.causeMessage}`)
  108. } else if (diagnostic.errorMessage) {
  109. details.push(`message=${diagnostic.errorMessage}`)
  110. }
  111. return `${diagnostic.stage} failed for ${subject} (${details.join(', ')})`
  112. }
  113. export function formatRetrySummary(diagnostic, delayLabel) {
  114. const subject = diagnostic.fileName || diagnostic.target || diagnostic.stage
  115. const details = [ `attempt ${Math.min(diagnostic.attempt + 1, diagnostic.maxAttempts)}/${diagnostic.maxAttempts}` ]
  116. if (diagnostic.fileIndex != null && diagnostic.totalFiles) {
  117. details.unshift(`file ${diagnostic.fileIndex}/${diagnostic.totalFiles}`)
  118. }
  119. if (diagnostic.httpStatus) {
  120. details.push(`status=${diagnostic.httpStatus}`)
  121. }
  122. if (diagnostic.causeCode) {
  123. details.push(`cause=${diagnostic.causeCode}`)
  124. }
  125. details.push(`in ${delayLabel}`)
  126. return `retrying ${diagnostic.stage} ${subject} (${details.join(', ')})`
  127. }
  128. export function buildDebugPayload(diagnostic, error) {
  129. const errorDetails = extractErrorDetails(error)
  130. return {
  131. diagnostic,
  132. error: {
  133. cause: errorDetails.rawCause,
  134. errorMessage: errorDetails.errorMessage,
  135. errorName: errorDetails.errorName,
  136. stack: errorDetails.stack,
  137. },
  138. }
  139. }
  140. export function attachDiagnostic(error, diagnostic) {
  141. const normalizedError = error instanceof Error ? error : new Error(String(error))
  142. normalizedError.diagnostic = diagnostic
  143. return normalizedError
  144. }