Перейти к содержимому

Автоматический перехват ​

Сразу после 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.