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

Обзор ​

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

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/browser'

export function colorSchemeIntegration(): Integration {
    return {
        name: 'color-scheme',
        setup(client) {
            if (typeof window === 'undefined') return

            const query = window.matchMedia('(prefers-color-scheme: dark)')
            const apply = () => client.setTag('color_scheme', query.matches ? 'dark' : 'light')

            apply()
            query.addEventListener('change', apply)

            this.teardown = () => query.removeEventListener('change', apply)
        },
    }
}

Два соглашения встроенных интеграций, которые стоит перенять:

  • Экспортируйте фабрику, а не объект. colorSchemeIntegration() возвращает новый объект при каждом вызове, так что состояние экземпляра (слушатели, счётчики, последнее значение) не перетекает между клиентами, а любая интеграция — встроенная или ваша — вызывается одинаково.
  • Проверяйте браузерные глобальные объекты. Проверяйте typeof window/document/navigator, прежде чем к ним обращаться, чтобы при выполнении бандла во время SSR или в тестах интеграция ничего не делала, а не падала.

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

В init() ​

ts
import { init } from '@buglapse/browser'
import { colorSchemeIntegration } from './buglapse/color-scheme'

init({
    dsn: '{dsn}',
    integrations: [colorSchemeIntegration()],
})

Список сливается со встроенными по умолчанию (сейчас это только globalErrorIntegration()). Элемент, name которого совпадает со встроенным, заменяет его — так интеграция с именем 'global-error' подменяет встроенный перехват ошибок без отдельного вызова disable().

Через use() ​

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

use(colorSchemeIntegration())

use() работает и до, и после init(): до — регистрация встаёт в очередь и применяется сразу после встроенных; после — setup() выполняется немедленно. Со script-тегом это Buglapse.use(...) на глобальном объекте.

use() принимает и фабрику с настройками — так регистрируются встроенные:

ts
use(slowRequestIntegration, { thresholdMs: 2000 })
// то же, что: use(slowRequestIntegration({ thresholdMs: 2000 }))

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

Отключение ​

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

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

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

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

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

МетодДля чего
client.setTag(key, value)Добавить тег для поиска во все будущие события
client.setContext(key, object)Прикрепить структурированный контекст ко всем будущим событиям
client.setUser(user)Определить текущего пользователя
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()

Контекст, теги и хлебные крошки описаны на страницах Контекст и Хлебные крошки; спаны — в Трейсинге.

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

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

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

class AbortedByUserError extends Error {}

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

    return {
        name: 'ignore-aborts',
        setup(client) {
            client.addPreprocessor((event, rawError) => {
                if (!enabled || event.type !== 'exception') return event
                if (rawError instanceof AbortedByUserError) return null
                if (rawError?.name === 'AbortError') return null

                return event
            })
        },
        teardown() {
            enabled = false
        },
    }
}

Препроцессоры нельзя снять — у addPreprocessor() нет обратной операции. Если интеграцию нужно уметь отключать, закройте препроцессор флагом, который переключает teardown(), как выше. Препроцессоры выполняются в порядке регистрации, и как только один вернул null, остальные пропускаются.

Если нужен разовый фильтр, а не переиспользуемый именованный плагин, вызовите addPreprocessor(fn) прямо на SDK, не оборачивая в интеграцию.

Подмена глобальных объектов ​

Интеграции, наблюдающие за страницей — сетевые запросы, навигация, события DOM, — обычно оборачивают браузерный API. Сохраните ссылку на оригинал и восстановите его в teardown(). Эта сообщает о любом fetch медленнее порога логом-предупреждением:

ts
import type { BuglapseClient, Integration } from '@buglapse/browser'

export interface SlowRequestConfig {
    thresholdMs?: number
}

// SDK отправляет свои пачки через fetch — никогда не сообщайте о них (и не инструментируйте их),
// иначе медленный /api/envelope будет бесконечно кормить себя логами «slow request».
function isOwnRequest(client: BuglapseClient, url: string): boolean {
    const endpoint = client.transport.endpoint
    if (!endpoint) return false

    const target = new URL(url, window.location.href)
    const own = new URL(endpoint)

    return target.origin === own.origin && target.pathname === own.pathname
}

export function slowRequestIntegration(config: SlowRequestConfig = {}): Integration {
    const thresholdMs = config.thresholdMs ?? 3000

    return {
        name: 'slow-request',
        setup(client) {
            if (typeof window === 'undefined' || typeof window.fetch !== 'function') return

            const originalFetch = window.fetch.bind(window)

            window.fetch = async (...args: Parameters<typeof fetch>) => {
                const [input, init] = args
                const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url

                if (isOwnRequest(client, url)) return originalFetch(...args)

                const startedAt = performance.now()
                const response = await originalFetch(...args)
                const duration = Math.round(performance.now() - startedAt)

                if (duration >= thresholdMs) {
                    client.log.warn(`Slow request: ${init?.method ?? 'GET'} ${url} took ${duration}ms`, {
                        url,
                        method: init?.method ?? 'GET',
                        status: response.status,
                        duration,
                    })
                }

                return response
            }

            this.teardown = () => {
                window.fetch = originalFetch
            }
        },
    }
}

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

  • Не глотайте исходное поведение. Всегда вызывайте оригинал и возвращайте (или пробрасывайте) его результат без изменений — интеграция должна быть незаметна для приложения.
  • Пропускайте трафик SDK. Сравнивайте с client.transport.endpoint, как выше. Без этого инструментирование fetch/XMLHttpRequest может сообщать о собственных запросах SDK, а падающий адрес envelope превращается в петлю обратной связи.
  • Присваивание this.teardown внутри setup() — самый простой способ захватить значения (originalFetch, ссылки на слушатели), которые существуют только после setup. Это работает, потому что клиент вызывает setup() на самом объекте интеграции.
  • Один API могут оборачивать несколько интеграций. Встроенные breadcrumbsIntegration и httpErrorsIntegration оборачивают fetch независимо; ваша станет ещё одним слоем. Отключайте интеграции, подменяющие одну функцию, в порядке, обратном регистрации, иначе более поздний teardown() может восстановить устаревшую обёртку.

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

client.captureException(), client.log.* и помощники спанов покрывают почти всё. Для встроенного типа события без отдельного метода есть низкоуровневый выход:

ts
client.capture('session', { sid, status: 'errored', errors: 1 })

capture(type, payload) проставляет в payload текущие теги/контексты/трейс и отправляет его через тот же конвейер препроцессоров и транспорт, что и всё остальное. type должен быть из типов, которые принимает /api/envelope (exception, log, span, replay, session), а payload — соответствовать форме этого типа; где есть отдельный метод, используйте его.

Отладка ​

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

Подключение через script-тег ​

Со script-тегом с CDN переключатели в Настройки → Скрипт загрузчика охватывают только встроенные интеграции. Свои регистрируются из вашего кода через глобальный объект:

html
<script src="https://app.buglapse.com/cdn/{project-token}/buglapse.min.js"></script>
<script>
    Buglapse.use({
        name: 'color-scheme',
        setup(client) {
            client.setTag('color_scheme', matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
        },
    })
</script>

Здесь script-тег загружается без async, так что window.Buglapse гарантированно существует, когда выполняется второй скрипт. Если оставляете async, регистрируйте из слушателя load на элементе <script> SDK.