🌳
pt0/serverF/throwErrorF/maybeNotifErrF.mts
4import { getEnvConf } from '../envConfF.mts'
5import { getPubEnv } from '../getPubEnvF.mts'
6import * as _ from 'lodash-es'
7import os from 'os'
8import { inspect } from 'util'
14import { unwrapErr } from './unwrapErrF.mts'
26import { tsAbsPath } from '../../ptDirF.mts'
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}) => {
47 let {
48 transformErrFnc, onErrFnc,
49 rlHistStore=ptDiskDir + '/expEmailRlHist.json'
50 } = errNotifCtx.getStore() || {}
54 if (transformErrFnc) {
55 err = transformErrFnc(err)
56 }
58 const errH = getClientErrH({err})
60 const {errHash, message, debugMsg, stdoutError, emailNotif} = errH
62 const git_sha = process.env.NEXT_PUBLIC_GIT_SHA
63 const {cluster_name} = getEnvConf()
64 const {gpSiteHost, gpIsProd} = getPubEnv()
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'})
72 const actErr: any = unwrapErr(err)
73 const silencedSample = actErr?.uniqDebugH
75 let rlHistH: Record<string, any> = expEmailRlHistH
76 if (rlHistStore) {
77 rlHistH = await readJsonIfExist(tsAbsPath(rlHistStore)) || {}
78 }
80 const {isLimited: isRlSilenced, backoffNo, numSilenced, silencedSamples = []} = getExpBackoffLimited({
81 cache_key: subjectBase, rlHistH, silencedSample
82 })
83 if (rlHistStore) {
84 await fs1Promises.writeFile(rlHistStore, JSON.stringify(rlHistH))
85 }
87 reqCtx ||= nextReqCtx.getStore() // when is this actually necessary?
88 const isTestException = reqCtx?.isTestException
89 const subject = subjectBase
91 if (reqCtx) {
92 if (reqCtx.req_url == apiGqlPath) {
93 delete reqCtx.req_url
94 }
95 if (err.path) {
96 reqCtx.gqlPath = err.path
97 }
98 }
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
104 if (dumpDebugH) {
105 ;({dontSkipEmailOnDev} = dumpDebugH)
106 delete dumpDebugH.dontSkipEmailOnDev // don't incl in debug info, it's just noise
107 }
109 let errCtx: pterrErrCtx = {
110 message, errHash,
111 backoffNo, numSilenced,
112 isRlSilenced,
113 ...(silencedSamples.length > 0 && {silencedSamples}),
114 }
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
121 }
123 const envConf = getEnvConf() as any
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'
129 ])
131 let envDebugInfoH = betInspect({
132 ...compactObjByVals({uniqDebugH, dumpDebugH}),
133 envCtx,
134 reqCtx: reqCtxPick,
135 errCtx
136 })
137 if (transformErrFnc) {
138 envDebugInfoH = transformErrFnc(envDebugInfoH, true)
139 }
141 let printH = {
142 ...envDebugInfoH
143 }
145 const errToStr = (actErr.toStringReplaced || actErr.toString()).trim()
146 let printA: unknown[] = []
147 if (isGqlErr(err)) {
148 printA.push(gqlErrToStr({err, inclTitleLine: false, inclStack: true}))
149 } else if (isJsErr(actErr)) {
150 printA.push(jsErrToStr({err: actErr, inclTitleLine: false, inclStack: true}))
151 } else {
152 printA.push(`err.toString(): ` + errToStr)
153 }
154 const gqlMatch = err.toStringReplaced?.match(/GraphQL request\:(.+)$/s)
155 if (gqlMatch) {
156 printA.push(gqlMatch[1])
157 }
159 const ignoreStartWith = [
160 'at new PublicVisError',
161 'at throwPubErr',
162 ]
163 const noisyPatterns = [
164 errToStr,
165 `GraphQLError: ${errToStr}`,
166 'process.processTicksAndRejections',
167 '/node_modules/lodash',
168 '/node_modules/knex',
169 '/dbCtxF.mts',
170 '/dbConnWrapF.mts',
171 'at new Promise (<anonymous>)',
172 ]
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
177 return true
178 })
180 let resolvedStackLines = stackLines
181 try {
182 resolvedStackLines = await mapStackTrace({stackA: stackLines})
183 } catch (smErr: any) {
184 console.warn('source map resolution failed', smErr.message)
185 }
187 if (resolvedStackLines.length > 0) {
188 printA.push(`err.stack: ` + resolvedStackLines.join("\n"))
189 }
191 printA.push(printH)
193 if (stdoutError) {
194 let doPrintA = printA
195 if (errH.isPublicVis && debugMsg) {
196 doPrintA = ['shorterr', debugMsg]
197 }
198 betLog(...doPrintA)
199 }
201 if (isRlSilenced) return
202 if (!emailNotif) return
204 if (onErrFnc) {
205 await onErrFnc({errHash: errHash!, context: reqCtx})
206 }
208 let textA = [
209 ...printA.map((arg) => {
210 if (_.isPlainObject(arg)) {
211 return inspect(arg, {
212 showHidden: false, depth: null,
213 breakLength: 300,
214 })
215 }
216 return arg
217 })
218 ]
220 textA.push(`---PTERR-JSON---`)
221 textA.push(JSON.stringify({
222 envCtx,
223 reqCtx: reqCtxPick,
224 errCtx,
225 ...compactObjByVals({uniqDebugH, dumpDebugH}),
226 ...(isJsErr(actErr) && {
227 clientStackA: actErr.stackA || [],
228 componentStackA: actErr.componentStackA || [],
229 smResolutionFailed: resolvedStackLines.some((l: string) => l.startsWith('utt') || l.includes('FAILED_TO_PARSE_LINE')),
230 }),
231 }))
232 textA.push(`---/PTERR-JSON---`)
234 const text = textA.join("\n")
236 const {fromEmail} = await getMailProps()
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) {
243 console.log(`${localExceptionSubjPrefix}${subject}
244emailbody: ${text}`)
245 console.log('skipped sendExceptionEmail because isDevPc (stop popup spam)')
246 // leave console.log^ otherwise debug tricky
247 return
248 }
250 try {
252 subject, text,
253 messageId,
254 inReplyTo, references: inReplyTo
255 })
256 } catch (smtpErr: any) {
257 console.error('sendExceptionEmail smtp failed', smtpErr?.message || smtpErr)
258 }