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

Обзор ​

Интеграция — небольшой плагин, который один раз, при регистрации, подключается к клиенту SDK. Каждая встроенная (httpServerIntegration, breadcrumbsIntegration, ...) построена на том же интерфейсе, что доступен вам, — всё, что делает встроенная, может и ваша.

ts
interface Integration {
    name: string
    setup(client: BuglapseClient): void
    teardown?(): void
}
  • name — уникальный id. По нему ищет disable(name), и он же решает, игнорируется ли повторная регистрация или заменяет существующую (см. Регистрация).
  • setup(client) — вызывается один раз, когда интеграция регистрируется на работающем клиенте. Здесь подписываются на события, подменяют модули, проставляют контекст или добавляют препроцессор.
  • teardown() — необязательно. Вызывается из disable(name); отменяет всё, что сделал setup().

Минимальная интеграция ​

Помечать каждое событие деплоем, из которого оно пришло, читая его из переменных окружения:

ts
import type { Integration } from '@buglapse/node'

export function deploymentIntegration(): Integration {
    return {
        name: 'deployment',
        setup(client) {
            client.setTag('region', process.env.FLY_REGION ?? 'local')
            client.setContext('deployment', {
                id: process.env.DEPLOYMENT_ID,
                commit: process.env.GIT_SHA,
            })
        },
    }
}

Экспортируйте фабрику, а не объект. deploymentIntegration() возвращает новый объект при каждом вызове, так что состояние экземпляра (слушатели, таймеры, счётчики) не перетекает между клиентами, а любая интеграция — встроенная или ваша — вызывается одинаково.

Регистрация ​

В init() ​

ts
import { init } from '@buglapse/node'
import { deploymentIntegration } from './buglapse/deployment'

init({
    dsn: process.env.BUGLAPSE_DSN,
    integrations: [deploymentIntegration()],
})

Список сливается со встроенными по умолчанию (uncaughtExceptionIntegration() и contextLinesIntegration()). Элемент, name которого совпадает со встроенным, заменяет его — так интеграция с именем 'uncaught-exception' подменяет встроенную обработку падений без отдельного вызова disable().

Через use() ​

ts
import { use } from '@buglapse/node'

use(deploymentIntegration())

use() работает и до init() (встаёт в очередь и применяется сразу после встроенных), и после (setup() выполняется немедленно). Принимает и фабрику с настройками — так регистрируются встроенные:

ts
use(slowQueryIntegration, { pool, thresholdMs: 500 })
// то же, что: use(slowQueryIntegration({ pool, thresholdMs: 500 }))

В отличие от init({ integrations }), use() никогда не заменяет: если интеграция с тем же именем уже зарегистрирована, новая игнорируется. Чтобы заменить, сначала вызовите disable(name).

Отключение ​

ts
import { disable } from '@buglapse/node'

disable('deployment') // вызывает teardown(), затем снимает регистрацию

До init() вызов disable(name) влияет только на встроенные интеграции — не даёт им зарегистрироваться вовсе.

Что может setup() ​

setup() получает тот же BuglapseClient, с которым работает плоский API:

МетодДля чего
client.setTag(key, value)Добавить тег для поиска во все будущие события (на весь процесс)
client.setContext(key, object)Прикрепить структурированный контекст ко всем будущим событиям (на весь процесс)
client.withScope(fn)Выполнить fn со своей областью, крошками и тегами
client.addBreadcrumb({ type, level, message, data })Записать то, что ведёт к следующему исключению
client.captureException(error)Сообщить об ошибке
client.log.info(message, attributes) (и log/warn/error/debug)Отправить лог
client.span() / client.asyncSpan() / client.startInactiveSpan()Замерить операцию спаном
client.addPreprocessor(fn)Переписать или отбросить события до отправки
client.flush()Сразу вытолкнуть накопленные события
client.transport.endpointАдрес envelope — чтобы не инструментировать собственные запросы SDK
client.debugБыл ли передан debug: true в init()

setTag() / setContext() на клиенте глобальны для процесса. Для всего, что относится к одному запросу или задаче, используйте withScope() — см. Теги и контекст.

Подписка на события процесса ​

Node многое сообщает через события process или объектов библиотек. Подписывайтесь в setup(), отписывайтесь в teardown(). Эта интеграция пересылает собственные предупреждения рантайма Node (устаревшие API, MaxListenersExceededWarning, ...) логами:

ts
import type { Integration } from '@buglapse/node'

export function processWarningsIntegration(): Integration {
    return {
        name: 'process-warnings',
        setup(client) {
            const onWarning = (warning: Error & { code?: string }) => {
                client.log.warn(warning.message, { name: warning.name, code: warning.code })
            }

            process.on('warning', onWarning)

            this.teardown = () => process.off('warning', onWarning)
        },
    }
}

Присваивание this.teardown внутри setup() — самый простой способ добраться до значений, которые существуют только после setup (слушатели, исходные функции). Это работает, потому что клиент вызывает setup() на самом объекте интеграции.

Таймер, запущенный в setup(), стоит пометить unref(), чтобы он сам по себе не держал процесс живым, — как делает EventLoopBlockIntegration.

Обёртка библиотеки ​

Чтобы трейсить или логировать вызовы библиотеки, оберните её метод и восстановите его в teardown(). Эта интеграция записывает каждый запрос пула pg спаном db и логирует медленные (только вызовы pool.query() с промисами — форма с колбэком не поддерживается):

ts
import type { Integration } from '@buglapse/node'
import type { Pool } from 'pg'

export interface SlowQueryConfig {
    pool: Pool
    thresholdMs?: number
}

export function slowQueryIntegration(config: SlowQueryConfig): Integration {
    const { pool, thresholdMs = 1000 } = config

    return {
        name: 'slow-query',
        setup(client) {
            const originalQuery = pool.query

            pool.query = async function (this: Pool, ...args: any[]) {
                const sql = typeof args[0] === 'string' ? args[0] : args[0]?.text
                const span = client.startInactiveSpan(sql?.split(' ', 1)[0] ?? 'query', { op: 'db' })
                span.setData('db.statement', sql)

                const startedAt = Date.now()

                try {
                    return await originalQuery.apply(this, args)
                } catch (error) {
                    span.setStatus('error')
                    throw error
                } finally {
                    const duration = Date.now() - startedAt
                    span.end()

                    if (duration >= thresholdMs) {
                        client.log.warn('Slow query', { sql, duration })
                    }
                }
            } as typeof pool.query

            this.teardown = () => {
                pool.query = originalQuery
            }
        },
    }
}

Что помнить при подмене:

  • Не меняйте исходное поведение. Вызывайте оригинал и возвращайте (или пробрасывайте) его результат без изменений.
  • Используйте startInactiveSpan() для вызова, который ожидается, пока выполняется другая работа, — он входит в активный трейс, не становясь активным спаном сам. См. Трейсинг → Листовые спаны.
  • Пропускайте трафик SDK, если инструментируете HTTP: сравнивайте адрес запроса с client.transport.endpoint, иначе каждая доставка событий будет сообщать о самой себе.
  • Одну функцию могут оборачивать несколько интеграций. Отключайте их в порядке, обратном регистрации, иначе более поздний teardown() может восстановить устаревшую обёртку.

Фильтрация и переписывание событий ​

Препроцессор видит каждое событие до отправки. Верните событие (возможно, изменённое), чтобы оставить его, или null, чтобы отбросить. Для событий exception второй аргумент — исходный Error, так что можно ветвиться по его классу:

ts
import type { Integration } from '@buglapse/node'

export function ignoreClientErrorsIntegration(): Integration {
    let enabled = true

    return {
        name: 'ignore-client-errors',
        setup(client) {
            client.addPreprocessor((event, error) => {
                if (!enabled || event.type !== 'exception') return event

                const status = (error as { statusCode?: number } | undefined)?.statusCode
                return status !== undefined && status < 500 ? null : event
            })
        },
        teardown() {
            enabled = false
        },
    }
}

Препроцессоры нельзя снять — закройте свой флагом, который переключает teardown(), как выше. Они выполняются в порядке регистрации, и как только один вернул null, остальные пропускаются. Для разового фильтра вызовите addPreprocessor(fn) из @buglapse/node напрямую.

Отправка других типов событий ​

Для встроенного типа события без отдельного метода есть низкоуровневый выход:

ts
client.capture('replay', { replayId, segmentId: 0, events })

capture(type, payload) проставляет в payload текущие теги, контекст и трейс и отправляет его через те же препроцессоры и транспорт, что и всё остальное. payload должен соответствовать форме этого типа (см. Формат событий); где есть отдельный метод, используйте его.

Отладка ​

С debug: true в init() клиент пишет каждую регистрацию интеграции (Integration "process-warnings" registered), каждый disable(), каждое пойманное событие и каждое событие, отброшенное препроцессором. Прежде чем добавлять собственное логирование, проверяйте client.debug.