From 3f7422e47cf4fabe845d85d72a2bf87d86141fea Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Thu, 9 Jul 2026 18:13:22 +0300 Subject: [PATCH] (doc): Sentry Observer integration wiki page (EN/RU) Cross-linked from Error-Handling and Recipes; not bundled as a package since Sentry tagging/noise filtering is opinionated enough that users will want to adjust it rather than depend on it. --- Error-Handling-RU.md | 1 + Error-Handling.md | 1 + Recipes-RU.md | 1 + Recipes.md | 1 + Sentry-Integration-RU.md | 38 +++++++++++++ Sentry-Integration.md | 117 +++++++++++++++++++++++++++++++++++++++ _Sidebar.md | 1 + 7 files changed, 160 insertions(+) create mode 100644 Sentry-Integration-RU.md create mode 100644 Sentry-Integration.md diff --git a/Error-Handling-RU.md b/Error-Handling-RU.md index 00ddcec..e69bddc 100644 --- a/Error-Handling-RU.md +++ b/Error-Handling-RU.md @@ -115,4 +115,5 @@ Internal-only ошибки идут по тому же logger path, но не с - [[Logging-RU]] - [[Middleware-RU]] +- [[Sentry-Integration-RU]] - [[Error-Handling]] diff --git a/Error-Handling.md b/Error-Handling.md index 9b5a8fd..b1d063c 100644 --- a/Error-Handling.md +++ b/Error-Handling.md @@ -230,3 +230,4 @@ plugin.NewPayload(func(ctx *laniakea.MessageContext, db *App) error { - [[Middleware]] - [[Logging]] - [[Commands-and-Plugins]] +- [[Sentry-Integration]] diff --git a/Recipes-RU.md b/Recipes-RU.md index 3b79702..a224181 100644 --- a/Recipes-RU.md +++ b/Recipes-RU.md @@ -59,4 +59,5 @@ Recipes хороши как стартовые шаблоны, но не зам - [[Getting-Started-RU]] - [[Commands-and-Plugins-RU]] - [[Localization-RU]] +- [[Sentry-Integration-RU]] - [[Recipes]] diff --git a/Recipes.md b/Recipes.md index 35a9dbe..55b167d 100644 --- a/Recipes.md +++ b/Recipes.md @@ -157,3 +157,4 @@ This is most useful when you want payload-type mismatches to fail loudly instead - [[Inline-Keyboards-and-Payloads]] - [[Drafts]] - [[Localization]] +- [[Sentry-Integration]] diff --git a/Sentry-Integration-RU.md b/Sentry-Integration-RU.md new file mode 100644 index 0000000..f8d1f89 --- /dev/null +++ b/Sentry-Integration-RU.md @@ -0,0 +1,38 @@ +# Sentry Integration RU + +English version: [[Sentry-Integration]] + +Это краткая русскоязычная версия страницы про интеграцию с Sentry. Полная и наиболее актуальная страница: [[Sentry-Integration]]. + +## Идея + +Laniakea намеренно не поставляет готовую интеграцию с Sentry. Точка расширения уже есть — интерфейс `Observer` (см. [[Error-Handling]]): в него прилетают все классифицированные ошибки из handler'ов, сцен, runner'ов и policy-проверок вместе с контекстом (plugin, handler kind, chat/user ID, update ID). Вызов observer'а panic-safe, так что упавший экспортёр не уронит бота. + +Тегирование и фильтрация шума — вещи специфичные под конкретный бот, поэтому такой код лучше держать у себя, а не тянуть `sentry-go` в зависимости библиотеки. + +## Что отправлять + +- `OnError` — срабатывает на каждую ошибку из центрального потока handler/scene. Пропускай `IsUserError(err)` — это ожидаемые ошибки, уже показанные пользователю, а не баги. +- `OnPolicyChecked` — срабатывает на каждую policy-проверку, включая обычные отказы "не разрешено". В Sentry стоит слать только `Internal`-ошибки (неправильная конфигурация, упавший запрос к API). +- `OnPollingRetry` — сам по себе не инцидент, но полезен как breadcrumb перед реальной ошибкой. + +## Пример + +Полный листинг — на странице [[Sentry-Integration]]. Кратко: реализуешь `laniakea.Observer`, у большинства методов пустое тело, а `OnError`/`OnPolicyChecked`/`OnPollingRetry` вызывают `sentry.CaptureException(...)` через `sentry.GetHubFromContext(ctx)`, добавляя теги (`plugin`, `handler_kind`, `handler_name`, `update_type`) и `chat_id`/`from_id`/`update_id` в `scope.SetContext(...)`. + +## Подключение + +```go +sentry.Init(sentry.ClientOptions{Dsn: dsn}) +defer sentry.Flush(2 * time.Second) + +bot.SetObserver(observability.SentryObserver{}) +``` + +`SetObserver(...)` полностью заменяет observer (см. [[Bot-Options-and-Configuration]]). Если нужно несколько получателей (Sentry + метрики + логирование), пиши небольшой fan-out `Observer`, оборачивая вызов каждого дочернего observer'а своим `recover()`, чтобы паника в одном не блокировала остальных для того же события. + +## Что читать дальше + +- [[Error-Handling-RU]] +- [[Recipes-RU]] +- [[Sentry-Integration]] diff --git a/Sentry-Integration.md b/Sentry-Integration.md new file mode 100644 index 0000000..848c3ea --- /dev/null +++ b/Sentry-Integration.md @@ -0,0 +1,117 @@ +# Sentry Integration + +Russian version: [[Sentry-Integration-RU]] + +Laniakea does not ship a Sentry integration, and intentionally so — see [[Error-Handling]] for the classified-error model and [[Runners]] / [[Policies]] for the other event sources. The `Observer` interface is already the extension point for this: it receives every classified handler, scene, runner, and policy error with rich context (plugin, handler kind, chat/user IDs, update ID), and dispatch is panic-safe, so a failing exporter cannot crash the bot. + +This page is a copy-paste starting point, not a bundled package. Sentry tagging conventions and noise filtering are opinionated enough that you'll likely want to adjust them for your bot, so keeping this out of the module avoids forcing `sentry-go` into every consumer's dependency graph for a shape that rarely fits as-is. + +## What to report + +- `OnError` fires for every error routed through the centralized handler/scene error flow. Skip `IsUserError(err)` results — those are expected, already answered in chat, and not bugs. +- `OnPolicyChecked` fires for every policy evaluation, including ordinary "not allowed" denials. Only the `Internal` ones (misconfiguration, failed API lookups) are worth reporting. +- `OnPollingRetry` is not an incident by itself, but makes a good breadcrumb leading up to a later error. + +## Example + +```go +package observability + +import ( + "context" + + "github.com/getsentry/sentry-go" + + laniakea "git.scuroneko.dev/scuroneko/laniakea" +) + +// SentryObserver forwards unexpected framework errors to Sentry. All hooks +// besides OnError, OnPolicyChecked and OnPollingRetry are no-ops — they +// exist only to satisfy laniakea.Observer. +type SentryObserver struct{} + +var _ laniakea.Observer = SentryObserver{} + +func (SentryObserver) OnUpdateReceived(context.Context, laniakea.UpdateReceivedEvent) {} +func (SentryObserver) OnUpdateHandled(context.Context, laniakea.UpdateHandledEvent) {} +func (SentryObserver) OnHandlerStarted(context.Context, laniakea.HandlerStartedEvent) {} +func (SentryObserver) OnHandlerFinished(context.Context, laniakea.HandlerFinishedEvent) {} +func (SentryObserver) OnSceneTransition(context.Context, laniakea.SceneTransitionEvent) {} +func (SentryObserver) OnRunnerFinished(context.Context, laniakea.RunnerFinishedEvent) {} + +// OnPollingRetry leaves a breadcrumb instead of an event — a single retry +// isn't an incident, but it's useful context if a real error follows. +func (SentryObserver) OnPollingRetry(ctx context.Context, e laniakea.PollingRetryEvent) { + sentry.GetHubFromContext(ctx).AddBreadcrumb(&sentry.Breadcrumb{ + Category: "polling", + Message: e.Err.Error(), + Level: sentry.LevelWarning, + Data: map[string]any{ + "attempt": e.Attempt, + "delay": e.Delay.String(), + }, + }, nil) +} + +// OnPolicyChecked reports only internal policy failures (misconfiguration, +// failed API lookups) — a plain "not allowed" denial is expected behavior. +func (SentryObserver) OnPolicyChecked(ctx context.Context, e laniakea.PolicyCheckedEvent) { + if e.Passed || e.Err == nil || !e.Internal { + return + } + captureLaniakeaError(ctx, e.Err, map[string]string{ + "source": "policy", + "policy": e.Name, + "plugin": e.Plugin, + }, e.FromID, e.ChatID, 0) +} + +// OnError is the main entry point: every handler/scene/runner error routed +// through the framework's classified-error flow lands here. +func (SentryObserver) OnError(ctx context.Context, e laniakea.ErrorEvent) { + if e.Err == nil || laniakea.IsUserError(e.Err) { + return // already surfaced to the user via ctx.Error, not a bug report + } + captureLaniakeaError(ctx, e.Err, map[string]string{ + "source": "handler", + "plugin": e.Plugin, + "handler_kind": string(e.HandlerKind), + "handler_name": e.HandlerName, + "update_type": string(e.UpdateType), + }, e.FromID, e.ChatID, e.UpdateID) +} + +func captureLaniakeaError(ctx context.Context, err error, tags map[string]string, fromID, chatID int64, updateID int) { + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + hub = sentry.CurrentHub().Clone() + } + hub.WithScope(func(scope *sentry.Scope) { + scope.SetTags(tags) + scope.SetContext("telegram", map[string]any{ + "chat_id": chatID, + "from_id": fromID, + "update_id": updateID, + }) + hub.CaptureException(err) + }) +} +``` + +## Wiring it up + +```go +sentry.Init(sentry.ClientOptions{Dsn: dsn}) +defer sentry.Flush(2 * time.Second) + +bot.SetObserver(observability.SentryObserver{}) +``` + +`bot.SetObserver(...)` replaces the observer wholesale — see [[Bot-Options-and-Configuration]]. If you need Sentry alongside metrics or logging instrumentation, write a small fan-out `Observer` that dispatches to several sub-observers, each wrapped in its own `recover()` so a panic in one exporter does not skip the others for the same event. + +## Related pages + +- [[Error-Handling]] +- [[Recipes]] +- [[Runners]] +- [[Policies]] diff --git a/_Sidebar.md b/_Sidebar.md index b1bdc73..db9555f 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -30,6 +30,7 @@ ## Practical Guides - [[Recipes]] +- [[Sentry-Integration]] - [[Testing-Bots-with-Laniakea]] ## Maintenance