Files
vercel__next.js/test/unit/TurbopackInternalError.test.ts
Benjamin Woodruff 3a5d293c38 feat(turbopack): Log anonymized internal error (panic) information to telemetry (#81272)
## Goals

- Pass through a custom `TurbopackInternalError` type when a Rust panic or an unexpected top-level `anyhow::Error` occurs.
- Make a best-effort attempt to log a location of the error to telemetry.
  - In the case of a panic, we should have the file, line, and column where the panic occured. This could be obscured inside of a helper method, but it's better than nothing. Since this is a position in Turbopack's source code, there's zero risk of user data being included here, so it should be safe to log to telemetry.

## How it works

In previous PRs, @wbinnssmith did the following:
- Added a thread-local for storing the most recent panic information in Rust
- Added `catch_unwind` logic to intercept the panic in turbo-tasks and read the thread-local information, with care take to make sure we're reading from the same thread (not across an `await` boundary).
- Added a custom `std::error::Error` type for internal errors, with support for storing information about a source panic.
- Ensured we set up `Error::source` correctly everywhere so that you can trace an error back to the root cause (presumably a panic).

In #80881, I added a `NextTurbopackContext` type that we can extend to store additional helpers on.

In this PR:
- Adds a callback to the `Project` constructor called `throwTurbopackInternalError` that constructs and throws a custom `TurbopackInternalError`. The definition of `TurbopackInternalError` must be in JS, as we cannot reasonably define the error subclass using NAPI.
- Wraps the callback in a `ThreadsafeFunction` so that it can be called in any of our async functions.
- Replaces all the places we were previously converting to `napi::Error` with logic to run `throwTurbopackInternalError`.

In a subsequent PR, we could move more of the panic handling into JS using `throwTurbopackInternalError`.

## Testing

Added a panic and ran:

```
NEXT_TELEMETRY_DEBUG=1 pnpm next dev --turbopack examples/basic-css/
```

<img src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/HAZVitxRNnZz8QMiPn4a/7f261d3f-351e-436a-9345-fd6e9056d20c.png" width="600">
2025-07-17 08:51:59 -07:00

49 lines
1.4 KiB
TypeScript

import path from 'path'
import os from 'os'
import {
throwTurbopackInternalError,
TurbopackInternalError,
} from 'next/dist/shared/lib/turbopack/internal-error'
import { Telemetry } from 'next/dist/telemetry/storage'
import { setGlobal } from 'next/dist/trace'
import { traceGlobals } from 'next/dist/trace/shared'
describe('TurbopackInternalError', () => {
it('sends a telemetry event when throwTurbopackInternalError() is called', async () => {
const oldTelemetry = traceGlobals.get('telemetry')
try {
const distDir = path.join(os.tmpdir(), 'next-telemetry')
const telemetry = new Telemetry({ distDir })
setGlobal('telemetry', telemetry)
const submitRecord = jest
// @ts-ignore
.spyOn(telemetry, 'submitRecord')
// @ts-ignore
.mockImplementation(() => Promise.resolve())
let internalError = null
try {
throwTurbopackInternalError(null, {
message: 'test error',
anonymizedLocation: 'file.rs:120:1',
})
} catch (err) {
internalError = err
}
expect(internalError).toBeInstanceOf(TurbopackInternalError)
expect(submitRecord).toHaveBeenCalledWith({
eventName: 'NEXT_ERROR_THROWN',
payload: {
errorCode: 'TurbopackInternalError',
location: 'file.rs:120:1',
},
})
} finally {
setGlobal('telemetry', oldTelemetry)
}
})
})