1// @ts-ignore - imapflow lacks type declarations 2import { ImapFlow } from 'imapflow' 17import * as _ from 'lodash-es' 19const getImapSecret = () => { 24export type ExceptionEmail = { 33export const fetchExceptions = async ({ limit = 50, includeBody = false, deployName, isTest = false, imapSecret }: { limit?: number, includeBody?: boolean, deployName?: string, isTest?: boolean, imapSecret?: { host: string, auth: { user: string, pass: string } } } = {}): Promise<ExceptionEmail[]> => { 34 const { host, auth } = imapSecret || getImapSecret() 36 const client = new ImapFlow({ 44 await client.connect() 46 const allFolders = await client.list() as { path: string, specialUse?: string }[] 47 const excludeSpecialUse = ['\\Sent', '\\Trash', '\\Junk', '\\Drafts'] 48 const foldersToSearch = allFolders.filter(f => !f.specialUse || !excludeSpecialUse.includes(f.specialUse)) 52 console.log(chalkYellow('No exceptionemail-token secret configured')) 56 const eeTokenSuffix = isTest ? '-test' : '-prod' 57 const eeTokenFull = eeToken + eeTokenSuffix 60 const msgs: ExceptionEmail[] = [] 61 const needBody = includeBody || !!deployName 62 const fetchOpts = needBody ? { envelope: true, source: true } : { envelope: true } 64 for (const folder of foldersToSearch) { 65 const lock = await client.getMailboxLock(folder.path) 67 const searchResults = await client.search({ header: { 'message-id': msgIdSearchPrefix } }) 68 if (!searchResults || searchResults.length === 0) continue 70 for await (const msg of client.fetch(searchResults, fetchOpts)) { 71 const subject = msg.envelope?.subject || '' 72 const date = msg.envelope?.date ? new Date(msg.envelope.date) : new Date(0) 73 const errHash = subject.split(' ')[0] || '' 74 const bodyRaw = msg.source?.toString() || '' 78 if (deployName && pterrJson?.envCtx?.deploymentName !== deployName) continue 85 bodyRaw: includeBody ? bodyRaw : '', 90 if (!err.responseText?.includes('No matching messages')) throw err 98 const seen = new Set<string>() 99 const deduped = msgs.filter(m => { 100 const key = `${m.subject}|${m.date.getTime()}` 101 if (seen.has(key)) return false 107 .sort((a, b) => b.date.getTime() - a.date.getTime()) 111export const printExceptionDetail = (msg: ExceptionEmail) => { 114 console.log(chalkGray(`Date: ${msg.date.toISOString()}`)) 117 const {clientStackA, componentStackA, smResolutionFailed, ...ctx} = pterrJson 120 const silencedSamples = pterrJson?.errCtx?.silencedSamples as unknown[] | undefined 121 if (silencedSamples?.length) { 122 console.log(chalkYellow(`\nAlso affected ${silencedSamples.length} silenced occurrence(s):`)) 123 for (const sample of silencedSamples) { 127 if (pterrJson?.componentStackA?.length) { 129 for (const line of pterrJson.componentStackA) { 130 console.log(` ${line}`) 133 if (pterrJson?.clientStackA?.length) { 135 for (const line of pterrJson.clientStackA) { 136 console.log(` ${line}`) 139 if (pterrJson?.smResolutionFailed) { 140 console.log(chalkYellow('\nâš Sourcemap resolution partially failed for this error')) 142 const decodedBody = msg.bodyRaw 143 .replace(/=\r?\n/g, '') 144 .replace(/=([0-9A-F]{2})/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) 145 const stackMatch = decodedBody.match(/err\.stack: ([\s\S]*?)(?=\n\{|\n---PTERR|\n\n|$)/) 148 console.log(stackMatch[1].trim()) 152export const exceptions = async () => { 158 const deployName = (all || showHash) ? undefined : appCfg?.name as string | undefined 160 console.log(chalkCyan(`Fetching recent exceptions from IMAP${deployName ? ` for ${deployName}` : ''}...`)) 162 const secret = getImapSecret() 164 console.log(chalkYellow(`No exceptionsImapSecretName configured in secretsMapping for this app`)) 167 console.log(chalkGray(`Connecting to ${secret.host} as ${secret.auth.user}...`)) 169 const exceptionMsgs = await fetchExceptions({ limit: 50, includeBody: true, deployName, isTest }) 172 const matches = exceptionMsgs.filter((m: ExceptionEmail) => m.errHash === showHash) 173 const msg = matches[0] 175 console.log(chalkGray(`No exception with errHash ${showHash}`)) 178 printExceptionDetail(msg) 182 const dedupGroups = new Map<string, { msg: ExceptionEmail, count: number, totalOccurrences: number }>() 183 for (const msg of exceptionMsgs) { 184 const sha = (msg.pterrJson?.envCtx?.git_sha as string)?.slice(0, 7) || '' 185 const key = `${msg.errHash}|${sha}` 186 const numSilenced = (msg.pterrJson?.errCtx?.numSilenced as number) || 0 187 const existing = dedupGroups.get(key) 190 existing.totalOccurrences += numSilenced + 1 192 dedupGroups.set(key, { msg, count: 1, totalOccurrences: numSilenced + 1 }) 195 const deduped = [...dedupGroups.values()] 197 let deployedGitSha = '', deployedBuildSha = '' 198 if (appCfg?.cluster_name && appCfg?.resourceTmpl) { 200 const res = await read2Resource({resource: appCfg.resourceTmpl as any, cluster_name: appCfg.cluster_name}) 201 deployedGitSha = (_.get(res, kubeResGitShaPath) as unknown as string)?.slice(0, 7) || '' 202 deployedBuildSha = (_.get(res, kubeResBuildShaPath) as unknown as string)?.slice(0, 7) || '' 204 console.log(chalkGray(`Could not resolve deployed sha from k8s: ${err.message}`)) 207 if (!deployedGitSha) { 208 const shaFromExceptions = deduped.find(d => (d.msg.pterrJson?.envCtx?.git_sha as string)?.slice(0, 7)) 209 deployedGitSha = (shaFromExceptions?.msg.pterrJson?.envCtx?.git_sha as string)?.slice(0, 7) || '' 211 const isCurrentDeploy = (sha: string) => sha === deployedGitSha || sha === deployedBuildSha 213 console.log(chalkCyan(`\nRecent exceptions (${deduped.length} groups, ${exceptionMsgs.length} total):`)) 215 for (let i = 0; i < deduped.length; i++) { 216 const { msg, totalOccurrences } = deduped[i] 218 const sha = (msg.pterrJson?.envCtx?.git_sha as string)?.slice(0, 7) || '' 220 const countCol = totalOccurrences > 1 ? chalkYellow(` (${totalOccurrences})`) : '' 221 console.log(` ${chalkGray(`${i + 1}.`)} ${chalkGray(ago)}${shaCol}${countCol} ${msg.subject.slice(0, 60)}`) 224 const unparseableFromCurrentDeploy = deduped.filter(d => { 225 const sha = (d.msg.pterrJson?.envCtx?.git_sha as string)?.slice(0, 7) 226 if (!isCurrentDeploy(sha)) return false 227 return !d.msg.pterrJson 229 for (const { msg } of unparseableFromCurrentDeploy) { 230 console.log(chalkYellow(` âš ${msg.errHash} from current deploy has unparseable body`)) 233 if (deduped.length === 0) { 236 console.log(chalkGray(`\nUse --show=<errHash> to see full details`)) 238 if (deployedGitSha) { 239 const buildPart = deployedBuildSha && deployedBuildSha !== deployedGitSha ? ` build:${chalkGreen(deployedBuildSha)}` : '' 244exceptions.cliSchema = { 245 show: { type: 'string' as const, hint: 'errHash', desc: 'show full details for exception by errHash' }, 246 all: { flag: true as const, desc: 'show exceptions for all apps' }, 247 test: { flag: true as const, desc: 'search for test exceptions (testdeploy) instead of prod' }, 249exceptions.cliDescript = 'exceptions - fetch recent exception emails from IMAP' 250exceptions.needsEnvConf = true