Официальные 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 |
|---|---|---|
| Ошибку | exception | name, message и frames или stack |
| Строку лога | log | level, message |
| Замеренную операцию | span | uuid, name, traceId, startTime, duration |
| Визит, для здоровья релизов | session | sid, 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задаёте вы: для JavaScriptstackего вычисляет сервер, для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 неверный |
403 | Origin не разрешён для проекта | Прекратить отправку; см. Запрос |
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. - Никогда не бросает исключений в приложение, за которым следит, и не блокирует его.