6import * as _ from 'lodash-es' 8import { inspect } from 'util' 30export const exceptionMsgIdPrefix = 'pterr' 32export const isExceptionThreadKey = (inReplyTo: string | undefined): string | undefined => { 33 if (!inReplyTo) return undefined 34 const match = inReplyTo.match(new RegExp(`<${exceptionMsgIdPrefix}\\.[^@]+@`)) 35 return match ? inReplyTo : undefined 38export const isTestExceptionThread = (headerValue: string | undefined): boolean => { 39 if (!headerValue) return false 40 return new RegExp(`^<${exceptionMsgIdPrefix}\\.[^.]+-test\\.`).test(headerValue) 43const expEmailRlHistH: Record<string, any> = {} 45export const maybeNotifError = async ({err, reqCtx}: {err: any, reqCtx?: any}) => { 48 transformErrFnc, onErrFnc, 49 rlHistStore=ptDiskDir + '/expEmailRlHist.json' 50 } = errNotifCtx.getStore() || {} 54 if (transformErrFnc) { 55 err = transformErrFnc(err) 60 const {errHash, message, debugMsg, stdoutError, emailNotif} = errH 62 const git_sha = process.env.NEXT_PUBLIC_GIT_SHA 65 const hostname = os.hostname() 67 const subjectA = [errHash] 69 subjectA.push(debugMsg || message) // message usually== errHash here 70 const subjectBase = _.truncate(subjectA.join(' '), {length: 400, omission: 'truncated'}) 73 const silencedSample = actErr?.uniqDebugH 75 let rlHistH: Record<string, any> = expEmailRlHistH 81 cache_key: subjectBase, rlHistH, silencedSample 84 await fs1Promises.writeFile(rlHistStore, JSON.stringify(rlHistH)) 87 reqCtx ||= nextReqCtx.getStore() // when is this actually necessary? 88 const isTestException = reqCtx?.isTestException 89 const subject = subjectBase 92 if (reqCtx.req_url == apiGqlPath) { 96 reqCtx.gqlPath = err.path 100 let {dumpDebugH, uniqDebugH} = actErr 101 const eeToken = (getPlainSecOpt(tsSec('exceptionemail-token')) ?? (console.warn('missing exceptionemail-token'), 'no-token')) + (isTestException ? '-test' : '-prod') 102 uniqDebugH = {...uniqDebugH, eeToken} 103 let dontSkipEmailOnDev = false 105 ;({dontSkipEmailOnDev} = dumpDebugH) 106 delete dumpDebugH.dontSkipEmailOnDev // don't incl in debug info, it's just noise 111 backoffNo, numSilenced, 113 ...(silencedSamples.length > 0 && {silencedSamples}), 116 if (!isVeryVerbose) { 117 if (errCtx.message == errCtx.errHash) delete errCtx.message 118 if (errCtx.isRlSilenced === false) delete errCtx.isRlSilenced 119 if (errCtx.backoffNo == 0) delete errCtx.backoffNo 120 if (errCtx.numSilenced == 0) delete errCtx.numSilenced 124 const deployEp = envConf.deployEp || '' 125 const envCtx: pterrEnvCtx = { gpIsProd: gpIsProd as boolean | null | undefined, appname: getAppName(), deploymentName: envConf.deploymentName || '', git_sha, hostname, deployEp, ...compactObjByVals({gpSiteHost, cluster_name}) } 126 const reqCtxPick = _.pick(reqCtx, [ 127 'gqUserTok', 'sessId', 128 'ip_address', 'user_agent', 'req_host', 'req_url', 'gqlPath' 137 if (transformErrFnc) { 138 envDebugInfoH = transformErrFnc(envDebugInfoH, true) 145 const errToStr = (actErr.toStringReplaced || actErr.toString()).trim() 146 let printA: unknown[] = [] 148 printA.push(gqlErrToStr({err, inclTitleLine: false, inclStack: true})) 150 printA.push(jsErrToStr({err: actErr, inclTitleLine: false, inclStack: true})) 152 printA.push(`err.toString(): ` + errToStr) 154 const gqlMatch = err.toStringReplaced?.match(/GraphQL request\:(.+)$/s) 156 printA.push(gqlMatch[1]) 159 const ignoreStartWith = [ 160 'at new PublicVisError', 163 const noisyPatterns = [ 165 `GraphQLError: ${errToStr}`, 166 'process.processTicksAndRejections', 167 '/node_modules/lodash', 168 '/node_modules/knex', 171 'at new Promise (<anonymous>)', 173 const stackLines = (actErr.stack || '').split("\n").map((s: string) => s.trim()).filter(Boolean).filter((line: string) => { 174 if (line.startsWith('uniqDebugH:')) return false 175 if (ignoreStartWith.some(prefix => line.startsWith(prefix))) return false 176 if (noisyPatterns.some(pattern => line.includes(pattern))) return false 180 let resolvedStackLines = stackLines 183 } catch (smErr: any) { 184 console.warn('source map resolution failed', smErr.message) 187 if (resolvedStackLines.length > 0) { 188 printA.push(`err.stack: ` + resolvedStackLines.join("\n")) 194 let doPrintA = printA 195 if (errH.isPublicVis && debugMsg) { 196 doPrintA = ['shorterr', debugMsg] 201 if (isRlSilenced) return 202 if (!emailNotif) return 205 await onErrFnc({errHash: errHash!, context: reqCtx}) 209 ...printA.map((arg) => { 210 if (_.isPlainObject(arg)) { 211 return inspect(arg, { 212 showHidden: false, depth: null, 220 textA.push(`---PTERR-JSON---`) 221 textA.push(JSON.stringify({ 227 clientStackA: actErr.stackA || [], 228 componentStackA: actErr.componentStackA || [], 229 smResolutionFailed: resolvedStackLines.some((l: string) => l.startsWith('utt') || l.includes('FAILED_TO_PARSE_LINE')), 232 textA.push(`---/PTERR-JSON---`) 234 const text = textA.join("\n") 238 const fromDomain = fromEmail.split('@')[1] || 'localhost' 239 const messageId = `<${[exceptionMsgIdPrefix, eeToken, errHash, backoffNo, Date.now().toString(36)].join('.')}@${fromDomain}>` 240 const inReplyTo = `<${[exceptionMsgIdPrefix, eeToken, errHash].join('.')}@${fromDomain}>` 242 if (isDevPc && !dontSkipEmailOnDev) { 245 console.log('skipped sendExceptionEmail because isDevPc (stop popup spam)') 246 // leave console.log^ otherwise debug tricky 254 inReplyTo, references: inReplyTo 256 } catch (smtpErr: any) { 257 console.error('sendExceptionEmail smtp failed', smtpErr?.message || smtpErr)