@yaebal/sentry

sentry error tracking: an isolation scope per update with chat and user, failing handlers captured with that scope, and breadcrumbs for every update and bot api call.

install

terminal
pnpm add @yaebal/sentry @sentry/node

pass the sentry sdk you already use — @sentry/node, @sentry/bun, @sentry/deno, @sentry/cloudflare. the plugin reads it structurally, so it has no sentry dependency of its own and never fights your sdk version.

usage

bot.ts
import * as Sentry from "@sentry/node";
import { Bot } from "@yaebal/core";
import { sentry } from "@yaebal/sentry";

Sentry.init({ dsn: process.env.SENTRY_DSN });

// install first, so its scope wraps every other plugin
const bot = new Bot(process.env.BOT_TOKEN!).install(sentry({ sentry: Sentry, setUser: true }));

bot.command("pay", async (ctx) => {
  ctx.sentry.setTag("feature", "payments");
  try {
    await charge(ctx.from!.id);
  } catch (error) {
    ctx.sentry.captureException(error); // handled, but still worth a report
    await ctx.reply("payment failed, try again later");
  }
});

what lands on ctx

fielddoes
ctx.sentry.scopethis update's isolation scope
ctx.sentry.captureException(error) / captureMessage(text)report with this update's scope
ctx.sentry.setTag(key, value) / setExtra(key, value)annotate this update's scope

options

optiondefaultdoes
sentry—the initialized sentry sdk
setUserfalseattach the sender (id, username) as the sentry user — personal data, so opt-in
breadcrumbstruea breadcrumb per update and per bot api call (error-level for failures)

with bot.onError

a thrown error is captured, then rethrown, so bot.onError still answers the user. isReported(error) tells it the error already reached sentry:

errors.ts
import { isReported } from "@yaebal/sentry";

bot.onError((error, ctx) => {
  if (!isReported(error)) Sentry.captureException(error); // e.g. a contextFactory failure
  return ctx.send("something broke — we're on it");
});

related