Обзор
Интеграция — небольшой плагин, который один раз, при регистрации, подключается к клиенту 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.