Автоматический перехват
Сразу после init() UncaughtExceptionIntegration слушает события процесса uncaughtException и unhandledRejection. О падении сообщается с полным стеком, всё накопленное в буфере выталкивается, и процесс завершается с кодом 1 — так же, как без SDK, только теперь вы об этом знаете.
Отклонение, причина которого не Error (Promise.reject('nope')), оборачивается в Error, так что у него всё равно есть имя, сообщение и стек.
Ошибки, которые ловит фреймворк
Большинство ошибок на сервере никогда не становятся необработанными: Express, Fastify, Koa, NestJS и другие ловят throw внутри обработчика маршрута и превращают его в ответ 500. Сообщайте о них из обработчика ошибок фреймворка.
Express — регистрируйте middleware ошибок после всех маршрутов:
ts
import { captureException } from '@buglapse/node'
app.use((error, request, response, next) => {
captureException(error)
next(error) // оставить обработку Express (или отправить свой ответ)
})Fastify:
ts
fastify.setErrorHandler((error, request, reply) => {
if (!error.statusCode || error.statusCode >= 500) captureException(error)
reply.send(error)
})Проверка выше пропускает ошибки валидации и прочие 4xx — это ожидаемые ошибки клиента, а не баги. Применяйте ту же идею в любом фреймворке.
С включённой HttpServerIntegration ошибка, пойманная здесь, несёт адрес и метод запроса, хлебные крошки именно этого запроса и ссылку на его трейс.
Перехват вручную
Сообщайте о пойманной ошибке сами через captureException():
ts
import { addBreadcrumb, captureException } from '@buglapse/node'
addBreadcrumb({ type: 'payment', level: 'info', message: 'Charging card', data: { orderId } })
try {
await chargeCard(order)
} catch (error) {
captureException(error)
throw error
}С включённой по умолчанию ContextLinesIntegration строки кода получают и кадры причин.
captureException() принимает Error. Если библиотека бросает что-то другое (строку, простой объект), сначала оберните: captureException(new Error(String(reason))).
Поймать и пробросить дальше — нормально: если проброшенная ошибка останется необработанной, о ней сообщат второй раз — включите DedupeIntegration, чтобы отбросить дубликат.
Причины и AggregateError
Цепочка cause едет вместе с ошибкой — до 10 причин, начиная с внешней, у каждой свой стек:
ts
try {
await loadCart()
} catch (error) {
captureException(new Error('Checkout could not start', { cause: error }))
}Страница инцидента показывает причины под ошибкой, и группировка учитывает их кадры. Причина, которая не Error ({ cause: 'timeout' }), показывается своим текстом. Члены AggregateError — с ним отклоняется Promise.any() — отправляются так же и показываются рядом с ошибкой; на группировку они не влияют.
Сообщения
captureMessage() сообщает о том, что заслуживает инцидента, но не является Error, — без стека, группируется по тексту:
ts
captureMessage('Payment provider returned an empty response', { level: 'warning' })level — debug, info (по умолчанию), warning, error или fatal; отправляется как тег level. Сообщение ниже error не делает запрос ошибочным. fingerprint работает так же, как у captureException().
Сообщение с переменными частями открывало бы инцидент на каждое значение. Напишите его через fmt — и оно сгруппируется по шаблону, Payment %s declined for %s:
ts
import { captureMessage, fmt } from '@buglapse/node'
captureMessage(fmt`Payment ${payment.id} declined for ${user.email}`)Для строки, которая не должна становиться инцидентом, используйте логи.
Номер для пользователя
captureException() и captureMessage() возвращают id события — UUID, который можно показать в диалоге ошибки или приложить к обращению в поддержку:
ts
const eventId = captureException(error)
showError(`Something went wrong. Reference: ${eventId}`)Вставьте его в поиск в Исключениях как event_id:<id> (или передайте инструменту get-exception в MCP), чтобы открыть именно это событие. lastEventId() возвращает id последней отправленной ошибки или сообщения — для кода, который не ловил её сам, например error boundary, показывающего то, о чём сообщил глобальный обработчик. Id равен undefined, если событие отброшено beforeSend или интеграцией, и если init() ещё не вызван.
Фоновые задачи и скрипты
Воркеры очередей, cron-задачи и скрипты не проходят через HTTP-запрос, поэтому дайте каждой единице работы свою область через asyncSpan() (или withScope()) и сообщайте об ошибках сами:
ts
import { asyncSpan, captureException } from '@buglapse/node'
worker.process(async (job) => {
await asyncSpan(`job ${job.name}`, async () => {
try {
await handle(job)
} catch (error) {
captureException(error)
throw error // пусть очередь повторит задачу
}
}, { op: 'queue.process' })
})Каждое событие, пойманное внутри, несёт хлебные крошки и трейс своей задачи, даже когда параллельно выполняются другие. См. Теги и контекст.
Скрипт, который завершается сразу после ошибки, должен сначала вытолкнуть буфер, иначе отчёт может так и не покинуть процесс:
ts
import { captureException, flush } from '@buglapse/node'
try {
await migrate()
} catch (error) {
captureException(error)
await flush()
process.exit(1)
}Подробности для одной ошибки
withScope() накладывает теги и контекст на всё, что поймано внутри его колбэка, через все await, не трогая глобальные значения:
ts
import { captureException, withScope } from '@buglapse/node'
await withScope(async (scope) => {
scope.setTag('tenant', tenant.slug)
scope.setContext('import', { file: file.name, rows: file.rows })
try {
await importFile(file)
} catch (error) {
captureException(error)
}
})Своя группировка
Ошибки группируются в инциденты по шаблону fingerprint проекта. Если для каких-то ошибок это не то, что нужно, передайте свой fingerprint — '{{ default }}' в нём означает группировку проекта:
ts
import { captureException, setFingerprint } from '@buglapse/node'
// Один инцидент, где бы ни была брошена ошибка.
captureException(error, { fingerprint: ['payment-gateway-down'] })
// Обычная группировка, но отдельно для каждого арендатора.
captureException(error, { fingerprint: ['{{ default }}', tenant.slug] })
// Всё, что поймано с этого момента; `null` возвращает группировку проекта.
setFingerprint(['{{ default }}', 'checkout'])scope.setFingerprint() делает то же внутри withScope(). Работает на тарифах со своей группировкой — см. Группировка из кода.
Меньше шума
Две интеграции отбрасывают исключения до отправки — и до того, как они засчитаются в квоту:
- DedupeIntegration — отбрасывает исключение, идентичное (имя, сообщение, стек) пойманному прямо перед ним.
- InboundFiltersIntegration — отбрасывает исключения, сообщение которых совпадает с шаблоном.
Для чего-то более точного добавьте препроцессор. Он видит исходный объект Error, так что можно фильтровать по классу:
ts
import { addPreprocessor } from '@buglapse/node'
addPreprocessor((event, error) => {
if (event.type === 'exception' && error instanceof NotFoundError) return null
return event
})На стороне сервера исключения можно отбрасывать выражением фильтра в Настройки → Фильтры проекта.
Неудачные исходящие запросы
Для fetch() ответ 500 от внешнего API — обычный ответ, ничего не бросается. HttpErrorsIntegration сообщает о таких ответах и сетевых сбоях логами уровня error (в разделе Логи, не в Исключениях).
Исключения в панели
Каждое исключение появляется в разделе Исключения проекта и группируется в инцидент по fingerprint, так что повторы одной ошибки сворачиваются в одну запись со счётчиком и временем первого и последнего появления, а не заваливают список. См. Инциденты.
Кадры стека указывают на файлы сервера, как они лежат на диске. Если вы собираете или минифицируете серверный код, задайте release и загрузите source maps этой сборки — см. Source maps.