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

Официальные SDK покрывают браузер и Node.js. Всё, что они делают, сводится к одному HTTP-запросу — JSON-пачке событий, отправленной на /api/envelope, — так что в Buglapse может отправлять что угодно, что умеет делать HTTPS-запросы: бэкенд на PHP или Python, сервис на Go, shell-скрипт в CI.

Эта страница — контракт, которому должен следовать клиент. Форма каждого типа события описана на его странице в разделе Формат событий; здесь — как доставить их на сервер.

Сначала проверьте Sentry SDK

Если для вашего языка есть официальный Sentry SDK, писать ничего не придётся. С включённой для организации интеграцией Sentry SDK у каждого проекта в Настройки → Общие появляется второй DSN — Sentry DSN; подставьте его в опцию dsn Sentry SDK без других изменений кода. См. Переход с Sentry.

Внутри JavaScript-рантайма новый SDK тоже не нужен: свой транспорт или своя интеграция меняют то, как @buglapse/* отправляет или ловит события.

Минимальный клиент ​

Одна строка лога из терминала:

bash
curl -X POST https://app.buglapse.com/api/envelope \
    -H 'X-Buglapse-Auth: Buglapse buglapse_key={key}' \
    -H 'Content-Type: application/json' \
    -A 'my-sdk/0.1.0' \
    -d '{
        "events": [{
            "type": "log",
            "timestamp": "2026-10-10T10:15:02.481Z",
            "tags": {},
            "contexts": { "environment": "production" },
            "payload": { "level": "info", "message": "Hello from curl" }
        }]
    }'
json
{ "success": true, "processed": 1, "filtered": 0, "sampled": 0, "quota_exceeded": 0, "rate_limited": 0, "invalid": 0, "disabled": 0 }

Лог уже в разделе Логи проекта. Остальная часть страницы — о том, что превращает этот запрос в SDK.

Назовите свой клиент

Отправляйте User-Agent с именем клиента, например my-sdk/0.1.0. Запрос без Origin и Referer считается серверным: его User-Agent не записывается как посетительский, и Фильтровать известных краулеров (Настройки → Фильтры) на него не смотрит — так что значение библиотеки по умолчанию (curl/…, python-requests/…, okhttp/…) не отфильтруется как бот. Из браузера фильтр, как и раньше, читает User-Agent запроса.

DSN ​

Клиент настраивается одной строкой — DSN проекта из Настройки → Общие:

https://{key}@{host}
  • Адрес — схема и хост DSN (с портом, если есть) плюс /api/envelope.
  • {key} — пользовательская часть, после URL-декодирования, — публичный ключ проекта. Только он определяет проект; ни id проекта, ни пути нет.

DSN без ключа или с путём считайте некорректным: выдайте предупреждение и ничего не отправляйте, а не бросайте исключение внутри приложения, за которым следите.

Запрос ​

http
POST https://app.buglapse.com/api/envelope
X-Buglapse-Auth: Buglapse buglapse_key={key}
Content-Type: application/json
User-Agent: my-sdk/0.1.0

{ "events": [ { … }, { … } ] }
  • Ключ передаётся в X-Buglapse-Auth. Там, где заголовки задать нельзя, работает и ?buglapse_key={key} в query.
  • Тело — JSON-объект с одним массивом events. В пачке можно смешивать типы. Ещё два ключа необязательны: sent_at — ваши часы в момент сериализации пачки, по которым сервер поправляет клиента со сбитыми часами (Расхождение часов), — и client_report, что клиент отбросил сам (Отчёты клиента).
  • Тело не сжимается — отправляйте обычный JSON.
  • Если у проекта есть Разрешённые домены (Настройки → Безопасность и приватность), Origin или Referer запроса должен совпадать с одним из них. Бэкенд не отправляет ни того, ни другого, поэтому его запросы отклоняются с 403 — очистите список или заведите отдельный проект для серверной части.

События ​

У каждого события одни и те же пять полей — см. envelope:

json
{
    "type": "exception",
    "timestamp": "2026-10-10T10:15:02.481Z",
    "tags": { "queue": "emails" },
    "contexts": { "environment": "production", "release": "api@2.3.0", "user.id": "42" },
    "payload": { … }
}

Строгостей у сервера мало. Событие некорректно — отбрасывается и учитывается в ответе — только если:

  • type не из списка exception, log, span, session, replay;
  • timestamp отсутствует или не разбирается (отправляйте ISO 8601 в UTC с миллисекундами);
  • это session без sid, с неизвестным status или нецелым errors.

Всё остальное снисходительно: отсутствующие tags или contexts считаются пустыми, значения null в них отбрасываются, а некорректная часть payload исключения обрезается, а не отклоняется. Так что проверяйте отправленное в панели, а не только по ответу.

Чтобы сообщитьОтправьтеМинимальный payload
Ошибкуexceptionname, message и frames или stack
Строку логаloglevel, message
Замеренную операциюspanuuid, name, traceId, startTime, duration
Визит, для здоровья релизовsessionsid, status, errors

replay — запись DOM браузера и имеет смысл только там.

Исключения ​

JavaScript SDK отправляют исходный текст stack, и сервер его разбирает. Любой другой язык должен отправлять стек структурированными frames и называть себя в platform:

json
{
    "name": "KeyError",
    "message": "'total'",
    "platform": "python",
    "frames": [
        { "function": "checkout", "module": "shop.cart", "file": "shop/cart.py", "abs_path": "/srv/app/shop/cart.py", "line": 88, "in_app": true },
        { "function": "dispatch", "module": "django.views", "file": "django/views/generic/base.py", "line": 142, "in_app": false }
    ],
    "breadcrumbs": []
}
  • frames начинаются с внутреннего кадра — кадр, бросивший ошибку, это frames[0]. Многие языки отдают стек в обратном порядке (traceback в Python — так); разверните его.
  • in_app задаёте вы: для JavaScript stack его вычисляет сервер, для frames — нет. Отмечайте код приложения true, а зависимости и рантайм — false: группировка использует только кадры приложения, если они есть, а панель сворачивает остальные.
  • platform — строчные буквы, цифры, _ и -, до 32 символов. Без него исключение считается JavaScript.
  • Цепочки ошибок (raise … from, getPrevious() в PHP) передаются в chain, начиная с внешней.
  • Чтобы дать пользователю номер ошибки, сгенерируйте UUID, отправьте его в поле event_id верхнего уровня и покажите: event_id: в поиске панели найдёт по нему событие.

Полный список полей, включая context (строки кода вокруг кадра), — на странице Exception.

Какие контексты задавать ​

contexts — плоский словарь с ключами через точку. Большинство хранятся как есть; задавать из клиента стоит вот эти:

  • environment, release — в каждом событии. Их читают фильтры, поиск по окружению и отслеживание релизов.
  • user.id, user.email, user.username, user.name — пользователь, к которому относится событие.
  • trace.id, span.id — в логе или исключении, пойманном во время активного спана, чтобы связать его с трейсом.
  • session.id — в каждом событии сессии, если вы отправляете сессии.

user.ip и user.agent всегда перезаписываются значениями самого запроса. Для бэкенд-клиента это адрес вашего сервера, а не пользователя. user.agent не хранится в событии: он сохраняется один раз, у пользователя проекта.

Спаны ​

Id генерируете вы, в формате W3C Trace Context: traceId — 32 шестнадцатеричные цифры в нижнем регистре, uuid спана — 16; случайные, не все нули (UUID с дефисами принимается как id трейса; как id спана он обрезается до первых 16 цифр). Спан, чей traceId не подходит, отклоняется как некорректный. uuid должен быть уникальным в пределах проекта, все спаны трейса разделяют его traceId, а дочерний указывает на родителя через parentId. Чтобы продолжить трейс, начатый другим сервисом, возьмите оба id из его заголовка traceparent (00-{trace id}-{parent span id}-{flags}) и передавайте свои дальше. У корневого спана трейса нет parentId. Отдельного события для самого трейса нет.

Если клиент сэмплирует трейсы, решайте на трейс, а не на спан, и кладите долю (0–1) в sampleRate спана — по ней экстраполируется число трейсов.

Ответы ​

СтатусЧто значитЧто делать
200, 202Пачка прочитана; в теле итогНичего — оба означают успех
202 с "duplicate": trueРовно это тело уже было принятоНичего
401Ключа нет или он неизвестенПрекратить отправку; DSN неверный
403Origin не разрешён для проектаПрекратить отправку; см. Запрос
429Ничего не прошло, сработал лимит частоты или квотаПодождать Retry-After секунд

Итог учитывает каждое событие пачки один раз:

json
{ "success": true, "processed": 18, "filtered": 1, "sampled": 0, "quota_exceeded": 0, "rate_limited": 0, "invalid": 1, "disabled": 0 }

События processed сохранены. disabled — тип, которого нет в тарифе организации. Остальные объясняются в разделе Что происходит с каждым событием. 200 не означает, что всё сохранено: invalid больше 0, пока вы разрабатываете клиент, — признак некорректного события.

Доставка ​

Что делают официальные SDK и что разумно взять по умолчанию:

  • Пачки. Накапливайте события и отправляйте при 20 или через 2 секунды без нового. Отправляйте сразу при исключении и перед выходом процесса.
  • Никогда не блокируйте и не ломайте приложение. Отправляйте вне пути обработки запроса, где язык это позволяет, используйте короткий таймаут (в SDK — 5 секунд) и глушите все свои собственные сбои.
  • Повторяйте только то, на что не пришёл ответ. При сетевой ошибке или таймауте отправьте пачку ещё раз. Любой ответ — включая 4xx и 5xx — окончателен: повтор его не изменит.
  • Повторяйте теми же байтами. Сериализуйте пачку один раз и отправляйте ровно это тело. Сервер помнит принятое тело 10 минут и отвечает на повтор "duplicate": true, так что повтор после потерянного ответа не сохранится и не тарифицируется дважды. Пересериализованная пачка с другим порядком ключей — новая пачка.
  • Не сообщайте о своих сбоях через клиент. Ошибка при отправке, пойманная и отправленная снова, зацикливается, пока сервер недоступен.

Ограничения частоты ​

Когда тип упирается в лимит — наплыв сверх аварийного ограничения, исчерпанный месячный пул, тариф без этой функции, — ответ говорит, какие типы перестать отправлять и на сколько. Заголовок приходит и с 200 и 202, а не только с 429:

X-Buglapse-Rate-Limits: 60:log;span, 3600:replay

Каждая группа через запятую — {секунды}:{тип};{тип}…. Пока время не вышло, отбрасывайте события этих типов до постановки в очередь — их всё равно отклонят.

429 без этого заголовка ограничивает все типы на Retry-After секунд (60, если нет и его).

X-Buglapse-Sample-Rates и X-Buglapse-Hot-Groups описывают динамическое сэмплирование. Клиент может игнорировать оба: сервер сэмплирует то, что ему прислали.

Образцовый клиент ​

Полный клиент для ошибок и логов на стандартной библиотеке Python — разбор DSN, структурированные кадры, пачки, один повтор и ограничения частоты. Ради краткости он отправляет в вызывающем потоке; настоящий SDK передал бы пачку фоновому воркеру.

python
import atexit, json, time, traceback, urllib.error, urllib.request
from datetime import datetime, timezone
from urllib.parse import unquote, urlsplit


class Buglapse:
    def __init__(self, dsn, environment="production", release=None):
        url = urlsplit(dsn)
        if not url.username or url.path.strip("/"):
            raise ValueError("Invalid DSN")
        self.endpoint = f"{url.scheme}://{url.netloc.rsplit('@', 1)[-1]}/api/envelope"
        self.key = unquote(url.username)
        self.contexts = {"environment": environment, "release": release}
        self.queue = []
        self.limits = {}  # тип события или "*" -> time.time(), до которого он придержан
        atexit.register(self.flush)

    def log(self, level, message, **attributes):
        self._add("log", {"level": level, "message": message, "attributes": attributes})

    def capture_exception(self, error):
        frames = [
            {
                "function": frame.name,
                "file": frame.filename,
                "abs_path": frame.filename,
                "line": frame.lineno,
                "in_app": "site-packages" not in frame.filename,
            }
            for frame in reversed(traceback.extract_tb(error.__traceback__))  # сначала внутренний
        ]
        self._add("exception", {
            "name": type(error).__name__,
            "message": str(error),
            "platform": "python",
            "frames": frames,
            "breadcrumbs": [],
        })
        self.flush()

    def _limited(self, type):
        now = time.time()
        return self.limits.get(type, 0) > now or self.limits.get("*", 0) > now

    def _add(self, type, payload):
        if self._limited(type):
            return
        self.queue.append({
            "type": type,
            "timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
            "tags": {},
            "contexts": self.contexts,
            "payload": payload,
        })
        if len(self.queue) >= 20:
            self.flush()

    def flush(self):
        events = [event for event in self.queue if not self._limited(event["type"])]
        self.queue = []
        if not events:
            return
        body = json.dumps({"events": events}).encode()  # сериализуется один раз: повтор шлёт те же байты
        request = urllib.request.Request(self.endpoint, data=body, headers={
            "X-Buglapse-Auth": f"Buglapse buglapse_key={self.key}",
            "Content-Type": "application/json",
            "User-Agent": "my-sdk/0.1.0",
        })
        for attempt in range(2):
            try:
                with urllib.request.urlopen(request, timeout=5) as response:
                    return self._rate_limits(response.status, response.headers)
            except urllib.error.HTTPError as error:  # сервер ответил: окончательно
                return self._rate_limits(error.code, error.headers)
            except Exception:  # ответа нет: один повтор, потом сдаёмся
                continue

    def _rate_limits(self, status, headers):
        now = time.time()
        header = headers.get("X-Buglapse-Rate-Limits")
        if header:
            for group in header.split(","):
                seconds, _, types = group.strip().partition(":")
                for type in filter(None, types.split(";")):
                    self.limits[type] = max(self.limits.get(type, 0), now + int(seconds or 60))
        elif status == 429:
            self.limits["*"] = now + int(headers.get("Retry-After") or 60)
python
buglapse = Buglapse("{dsn}", release="api@2.3.0")

buglapse.log("info", "Order placed", order_id=1042)

try:
    checkout(cart)
except Exception as error:
    buglapse.capture_exception(error)
    raise

Чек-лист ​

  • Разбирает DSN на адрес и ключ; некорректный DSN даёт предупреждение, а не исключение.
  • Отправляет X-Buglapse-Auth и собственный User-Agent.
  • Проставляет environment и release в каждом событии и пользователя, если он известен.
  • Отправляет исключения с platform и frames, начиная с внутреннего, с заданным in_app.
  • Собирает пачки, отправляет при исключении и при выходе.
  • Использует таймаут, повторяет сетевой сбой один раз тем же телом и считает любой ответ окончательным.
  • Соблюдает X-Buglapse-Rate-Limits и 429.
  • Никогда не бросает исключений в приложение, за которым следит, и не блокирует его.