diff --git a/site/netlify/functions/error-reporting.ts b/site/netlify/functions/error-reporting.ts index 7ffc344948e..4b5d6629a79 100755 --- a/site/netlify/functions/error-reporting.ts +++ b/site/netlify/functions/error-reporting.ts @@ -21,6 +21,40 @@ const USER_INPUT_ERROR_MESSAGE_PATTERNS: string[] = [ const isUserInputError = (message: unknown): boolean => typeof message === 'string' && USER_INPUT_ERROR_MESSAGE_PATTERNS.some((pattern) => message.includes(pattern)) +// `Bugsnag.notify()` discards the `stack` property of plain objects and substitutes a +// stacktrace captured here, inside this function. Rebuilding a real Error and assigning +// the stack sent by the CLI preserves the original frames from the user's machine. +const toError = ({ + cause, + message, + name, + stack, +}: { + cause?: unknown + message?: unknown + name?: unknown + stack?: unknown +}): Error => { + const error = new Error(typeof message === 'string' ? message : String(message)) + if (typeof name === 'string' && name !== '') { + error.name = name + } + if (typeof stack === 'string' && stack !== '') { + error.stack = stack + } + if (cause !== undefined) { + error.cause = cause + } + return error +} + +// Stack frames arrive with machine-specific install prefixes, e.g. +// file:///C:/Users/jane/AppData/Roaming/npm/node_modules/netlify-cli/dist/utils/x.js. +// Stripping everything up to the last `node_modules` makes the same frame identical +// across machines and install layouts (npm, npx, pnpm) so Bugsnag groups on it. +const normalizeFrameFile = (file: string): string => + file.replace(/^.*node_modules[/\\]/, '').replace(/\\/g, '/') + export const handler: Handler = async ({ body }) => { try { if (typeof body !== 'string') { @@ -43,13 +77,22 @@ export const handler: Handler = async ({ body }) => { return { statusCode: 200 } } - Bugsnag.notify({ name, message, stack, cause }, (event) => { + Bugsnag.notify(toError({ cause, message, name, stack }), (event) => { event.app = { releaseStage: 'production', version: cliVersion, type: 'netlify-cli', } + for (const error of event.errors) { + for (const frame of error.stacktrace) { + if (typeof frame.file === 'string') { + frame.file = normalizeFrameFile(frame.file) + frame.inProject = frame.file.startsWith('netlify-cli/') + } + } + } + for (const [section, values] of Object.entries(metadata)) { event.addMetadata(section, values as Record) }