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

Каждый 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; свой транспорт может читать что угодно из этого или вовсе игнорировать и жёстко задать свой адрес.