Каждый captureException(), log.*(), span() и всё остальное заканчивается одинаково: готовое событие передаётся транспорту клиента — единственной части, которая отвечает за его отправку в сеть. По умолчанию это пакетный транспорт на fetch(), направленный на /api/envelope этого проекта; что он делает и чем его заменить, настраивается опциями transport/transportOptions, описанными в Настройке.
По умолчанию: пакетный fetch-транспорт
init() сам собирает makeFetchTransport из dsn (в подключении через CDN он уже вшит в бандл). Транспорт не делает HTTP-запрос на каждое событие — он копит их и отправляет пачку одним POST https://app.buglapse.com/api/envelope с { events: [...] } и ключом DSN в заголовке X-Buglapse-Auth: Buglapse buglapse_key={key} — смотря что наступит раньше:
- буфер достиг
maxBatchSize(по умолчанию20), - в очередь встало событие типа из
immediateTypes(по умолчанию толькоexception— ошибку стоит отправить сразу, а не ждать таймера), - прошло
flushIntervalмс тишины после последнего события (по умолчанию2000), - что-то явно вызвало
Buglapse.flush(), или - вкладка ушла в фон или закрывается (см. Доставка при уходе со страницы ниже).
Завершившийся корневой span/asyncSpan тоже сам запускает отправку, так что весь трейс уходит вместе, как только готов, а не по каплям в ритме таймера пачек.
Повторы и сбои
Запрос, который не прошёл вовсе (сетевая ошибка, таймаут), по умолчанию повторяется один раз, после чего пачка бросается, — а ответ сервера 4xx/5xx не повторяется, потому что отклонённые данные или исчерпанная квота от повтора не исправятся. И то, и другое настраивается:
ts
Buglapse.init({
dsn: '{dsn}',
transportOptions: {
timeout: 5000, // мс до прерывания запроса
retries: 1, // дополнительные попытки после временного сетевого сбоя
},
})Когда пачка брошена, её события теряются — очереди на диске и повторов позже нет. Подключите onError, чтобы узнавать об этом, а не терять молча:
ts
Buglapse.init({
dsn: '{dsn}',
transportOptions: {
onError: (error, event) => {
console.warn('Buglapse dropped an event:', event.type, error.message)
},
},
})onError вызывается один раз на каждое потерянное событие, после исчерпания повторов. Не сообщайте изнутри него через сам Buglapse (Buglapse.captureException(error)) — сервер, который только что не ответил, скорее всего не примет и этот отчёт, поэтому SDK распознаёт и отбрасывает такой повторный вызов, чтобы не зациклиться.
Ограничения частоты
Когда сервер придерживает тип — наплыв сверх аварийного ограничения или исчерпанный месячный пул (см. События → Динамическое сэмплирование), — он отвечает заголовком X-Buglapse-Rate-Limits с затронутыми типами событий и временем ожидания или 429 с Retry-After. Тогда транспорт до истечения времени отбрасывает события этих типов ещё до постановки в очередь — настраивать ничего не нужно. Включите debug, чтобы видеть, когда это происходит.
Динамическое сэмплирование
Пока сервер сэмплирует тип (События → Динамическое сэмплирование), его ответы несут долю и самые горячие группы типа. Тогда транспорт сам сэмплирует события этих групп — по трейсу, сессии или записи, так же, как сервер, — а не отправляет то, что там всё равно отбросят. События любых других групп, логи-ошибки и спаны вне транзакции отправляются как обычно, так что новая ошибка всегда доходит до сервера. Настраивать нечего; включите debug, чтобы видеть отброшенное.
Что SDK отбрасывает сам
События, которые SDK отбросил до сервера, — спаны несэмплированного трейса, то, что отбросили beforeSend или интеграция, тип, который сервер попросил придержать, пачка, на которую не пришёл ответ, — подсчитываются и сообщаются со следующей пачкой как отчёт клиента, так что Статистика и использование показывает их рядом с тем, что отфильтровал или сэмплировал сам сервер. Buglapse.flush() и скрытие страницы отправляют накопленный отчёт, даже если событий для него нет.
Доставка при уходе со страницы
fetch(), уже идущий в момент закрытия вкладки или перехода, браузер может отменить до завершения, а запланированный таймером пачек может вообще не успеть начаться. Как только страница скрывается или выгружается (document.visibilityState становится "hidden"), транспорт сразу использует navigator.sendBeacon() для всего, что ещё в буфере, а не гонит fetch() наперегонки с выгрузкой, — это механизм браузера с гарантированной по возможности доставкой ровно для этого случая. sendBeacon не умеет задавать заголовки, поэтому ключ передаётся параметром ?buglapse_key=. По замыслу это «отправил и забыл» (без повтора и ответа) и используется только в этот момент; все остальные отправки идут через fetch().
Отправка вручную
Buglapse.flush() сразу отправляет текущую пачку и возвращает промис, который выполняется, когда на неё — и на любую ещё идущую пачку — пришёл ответ:
ts
Buglapse.addBreadcrumb({ type: 'ui.click', level: 'info', message: 'Clicked "Log out"' })
await Buglapse.flush()
window.location.href = '/logged-out'Вызывайте его перед тем, что может уничтожить страницу раньше срабатывания таймера, — ручной редирект, закрытие попапа, разбор маршрута SPA. Автоматическая доставка при уходе (выше) уже покрывает закрытие вкладки пользователем, так что это в основном для переходов, которые запускает ваш код.
Справочник transportOptions
Настраивает fetch-транспорт по умолчанию; со своим transport (см. ниже) все ключи игнорируются.
timeout— мс до прерывания идущего запроса. По умолчанию5000.retries— дополнительные попытки после временного сетевого сбоя, но не ответа 4xx/5xx. По умолчанию1.maxBatchSize— сколько событий копится до автоматической отправки. По умолчанию20.flushInterval— мс тишины после последнего события до автоматической отправки. По умолчанию2000.immediateTypes— типы событий, которые сразу отправляют всю пачку, не дожидаясь таймера или лимита размера. По умолчанию['exception'].onError— вызывается по разу на каждое событие, когда пачку не удалось отправить после исчерпания повторов. См. Повторы и сбои выше.
Замена транспорта
Передайте другую фабрику транспорта, чтобы отправлять события не на /api/envelope этого проекта, — чаще всего makeConsoleTransport, который просто пишет каждое событие в консоль, для работы локально без запущенного сервера:
ts
import { init, makeConsoleTransport } from '@buglapse/browser'
init({
dsn: '{dsn}',
transport: makeConsoleTransport,
})transport принимает саму фабрику, а не её вызов — makeConsoleTransport, а не makeConsoleTransport(). Со своим transport dsn уже не обязателен, а все ключи transportOptions игнорируются, потому что настраивать больше нечего.
Свой транспорт
Транспорт — любой объект такой формы:
ts
interface Transport {
send(event: BuglapseEvent): void | Promise<void>
flush?(options?: { report?: boolean }): void | Promise<void>
recordDiscard?(reason: DiscardReason, type: BuglapseEvent['type'], quantity?: number): void
readonly endpoint?: string
}send(event)— вызывается по разу на каждое пойманное событие. Что с ним делать, решаете вы — собрать в пачку, переслать, записать вlocalStorage, что угодно, что нужно нестандартному серверу.flush()— необязательно. Реализуйте, еслиsend()что-то буферизует, чтобыBuglapse.flush()и отправке в конце корневого спана было что выталкивать. Транспорт, отправляющий синхронно (какmakeConsoleTransport), может обойтись без него.recordDiscard()— необязательно. Клиент вызывает его для каждого события, отброшенного доsend()(beforeSend, интеграция, несэмплированный трейс); транспорт по умолчанию превращает эти счётчики вclient_reportпачки. Без него такие отбрасывания просто не сообщаются.endpoint— необязательно и нужно, только если вы включаете ещё и BreadcrumbsIntegration или HttpErrorsIntegration: обе подменяютfetch/XHR, чтобы превращать исходящие запросы в крошки/логи, и поendpointузнают и пропускают собственные запросы доставки SDK, чтобы те не возвращались шумом о самих себе.
Buglapse принимает фабрику, а не готовый экземпляр — (options) => Transport, — чтобы самому решать, когда транспорт создаётся:
ts
import { init, Transport, TransportOptions } from '@buglapse/browser'
function makeBeaconOnlyTransport(options: TransportOptions): Transport {
// DSN: https://{key}@{host}
const dsn = new URL(options.dsn!)
const endpoint = `${dsn.origin}/api/envelope`
const key = decodeURIComponent(dsn.username)
return {
endpoint,
send(event) {
const url = `${endpoint}?buglapse_key=${encodeURIComponent(key)}`
navigator.sendBeacon(url, JSON.stringify({ events: [event], sent_at: new Date().toISOString() }))
},
}
}
init({
dsn: '{dsn}',
transport: makeBeaconOnlyTransport,
})options — то, что складывается из dsn/debug верхнего уровня и ваших transportOptions; свой транспорт может читать что угодно из этого или вовсе игнорировать и жёстко задать свой адрес.