Skip to content

Bot API 9.4, Views System, OpenTelemetry & streamMessage

February 8 – 15, 2026

An absolutely packed week. GramIO catches up to Bot API 9.4 with 8 new contexts, ships a brand-new views/template system, launches OpenTelemetry and Sentry plugins, adds an onApiCall hook for API instrumentation, introduces streamMessage for live-typing drafts, brings button styling to keyboards, and delivers Node.js support for SQLite storage plus Bun-native Redis. Let's go.

gramio v0.4.14 — onApiCall Hook & Better Stack Traces

b54d121 54ece76 7e48c24

New hook: onApiCall for API call instrumentation

The 7th hook joins the family. onApiCall wraps the entire API call lifecycle, enabling tracing, logging, metrics — anything you want around every outgoing Telegram request. It works like middleware with next():

ts
bot.onApiCall(async (context, next) => {
    console.log(`→ ${context.method}`);
    const start = Date.now();
    const result = await next();
    console.log(`← ${context.method} (${Date.now() - start}ms)`);
    return result;
});

You can scope it to specific methods:

ts
bot.onApiCall("sendMessage", async (context, next) => {
    // only fires for sendMessage calls
    return next();
});

Multiple hooks compose like middleware — the first registered wraps everything. Works in both Bot and Plugin. This is what powers the new OpenTelemetry plugin under the hood.

Error stack traces now point to your code

TelegramError now captures the call site where you made the API call. When bot.api.sendMessage(...) fails, the stack trace points to your line of code, not framework internals. No config needed — it just works.

Dependencies

  • @gramio/contexts bumped to ^0.4.0
  • @gramio/keyboards bumped to ^1.3.0
  • @gramio/types bumped to ^9.4.0

@gramio/contexts v0.4.0 — Bot API 9.2 / 9.3 / 9.4

ea3049f aa3ff6d 3df81eb 524ba8b 13564e6

streamMessage — live-typing message drafts

Stream text chunks to the chat with live typing previews. Each chunk updates a draft in real-time via sendMessageDraft, and the message auto-finalizes via sendMessage when a 4096-character segment completes. Perfect for AI/LLM streaming responses:

ts
bot.command("stream", async (context) => {
    const chunks = generateTextChunks(); // Iterable or AsyncIterable
    const messages = await context.streamMessage(chunks);
});

Accepts Iterable<MessageDraftPiece> or AsyncIterable<MessageDraftPiece> where each piece is a string or { text, entities?, draft_id? }. Supports AbortSignal for cancellation.

Bot API 9.2 — Suggested posts & direct messages

8 new contexts and structures for the suggested posts lifecycle:

  • SuggestedPostApprovedContext, SuggestedPostApprovalFailedContext, SuggestedPostDeclinedContext, SuggestedPostPaidContext, SuggestedPostRefundedContext
  • DirectMessagesTopic structure
  • Chat.isDirectMessages, ChatFullInfo.parentChat, ChatAdministratorRights.canManageDirectMessages
  • Gift.publisherChat, UniqueGift.publisherChat
  • Message.suggestedPostInfo, Message.directMessagesTopic, Message.isPaidPost

Bot API 9.3 — Gift upgrades, new structures

  • GiftUpgradeSentContext — service message about gift upgrades
  • sendMessageDraft() method on SendMixin
  • New structures: GiftBackground, UniqueGiftColors, UserRating
  • Extended Gift with isPremium, background, uniqueGiftVariantCount
  • Extended ChatFullInfo with rating, uniqueGiftColors, paidMessageStarCount

Bot API 9.4 — Chat ownership, video quality, profile audios

  • ChatOwnerLeftContext, ChatOwnerChangedContext — track chat ownership changes
  • VideoQuality structure with codec ("h265" | "av01")
  • UserProfileAudios structure
  • Extended VideoAttachment with qualities getter
  • Extended UniqueGiftModel with rarity ("uncommon" | "rare" | "epic" | "legendary")
  • Extended UniqueGift with isBurned
  • Extended User with allowsUsersToCreateTopics()

@gramio/views — Template System for Reusable Message Views (NEW)

4de789b cd3b934 944a5fb c7521ae b2f917b

A brand-new package for building reusable message templates with automatic send/edit strategy detection. Define views once, render them anywhere — the library figures out whether to send a new message or edit the existing one based on the context type.

Programmatic views

ts
import { initViewsBuilder } from "@gramio/views";
import { defineAdapter } from "@gramio/views/define";

const adapter = defineAdapter({
    welcome(name: string) {
        return this.response
            .text(`Hello, ${name}!`)
            .keyboard([[{ text: "Start", callback_data: "start" }]]);
    },
});

const defineView = initViewsBuilder().from(adapter);

bot.derive(["message", "callback_query"], (context) => ({
    render: defineView.buildRender(context, {}),
}));

bot.command("start", (context) => context.render("welcome", "Alice"));

JSON-driven views

Define views as JSON with interpolation for text, keyboards, and media:

ts
import { createJsonAdapter } from "@gramio/views/json";

const adapter = createJsonAdapter({
    views: {
        welcome: {
            text: "Hello, {{name}}!",
            reply_markup: {
                inline_keyboard: [
                    [{ text: "Profile {{name}}", callback_data: "profile_{{id}}" }],
                ],
            },
        },
    },
});

Filesystem loading

ts
import { loadJsonViewsDir } from "@gramio/views/fs";

// views/messages.json → "messages.welcome", "messages.goodbye"
// views/goods/products.json → "goods.products.list"
const views = await loadJsonViewsDir("./views");

i18n support

Two approaches: adapter factory for per-locale JSON files, or custom resolve callback for translation keys:

ts
// Per-locale adapter selection
const defineView = initViewsBuilder<{ locale: string }>()
    .from((globals) => adapters[globals.locale]);

// Or custom resolve for translation keys
const adapter = createJsonAdapter({
    views: { greet: { text: "{{t:hello}}, {{name}}!" } },
    resolve: (key, globals) => {
        if (key.startsWith("t:")) return globals.t(key.slice(2));
    },
});

Supports all keyboard types (inline, reply, remove, force reply), single and grouped media with URL interpolation, and globals access via syntax.

@gramio/opentelemetry — Distributed Tracing (NEW)

5fe9928 56789d9 1ba7880

Vendor-neutral distributed tracing for GramIO using OpenTelemetry API. Every update becomes a root span, every API call becomes a child span — zero config, works with any OTEL backend (Jaeger, Grafana, Axiom, etc.).

ts
import { opentelemetryPlugin } from "@gramio/opentelemetry";

bot.extend(opentelemetryPlugin({
    recordApiParams: true, // record API params as span attributes
}));

Trace hierarchy:

gramio.update.message (CONSUMER)
  ├── telegram.api/sendMessage (CLIENT)
  ├── telegram.api/deleteMessage (CLIENT)
  └── custom spans via record()

Exported utilities: record(name, fn) for custom child spans, getCurrentSpan(), setAttributes(). Integrates seamlessly with Elysia webhooks — GramIO spans automatically nest under HTTP request spans.

@gramio/sentry — Error Tracking (NEW)

47948dd fb1e8c8

Sentry integration with automatic error capture, user identification, breadcrumbs, and optional tracing:

ts
import { sentryPlugin } from "@gramio/sentry";

bot.extend(sentryPlugin({
    setUser: true,      // auto-set user from context.from
    breadcrumbs: true,  // breadcrumb per update + API call
    tracing: false,     // per-update isolation scopes + spans
}));

// Derived context methods:
bot.command("test", (context) => {
    context.sentry.captureMessage("Something happened");
    context.sentry.setTag("custom", "value");
});

Uses @sentry/core for runtime-agnostic support (works in both Bun and Node.js).

@gramio/keyboards v1.3.0 — Button Styling

b97e34e

All button methods now accept an optional options parameter for visual styling:

ts
new InlineKeyboard()
    .text("Delete", "delete", { style: "danger" })
    .text("Confirm", "confirm", {
        style: "success",
        icon_custom_emoji_id: "5368324170671202286",
    });

Three styles: "danger" (red), "primary" (blue), "success" (green). Plus icon_custom_emoji_id for custom emoji icons next to button text. Works on both InlineKeyboard and Keyboard.

@gramio/types v9.4.0

177ce3d 27149c1

New types for Bot API 9.4: VideoQuality, UserProfileAudios, ChatOwnerLeft, ChatOwnerChanged, UniqueGiftModelRarity. Button styling types (KeyboardButtonStyle, InlineKeyboardButtonStyle) are now official. New API methods: getUserProfileAudios, setMyProfilePhoto, removeMyProfilePhoto.

@gramio/storage-sqlite v1.0.0 — Now Works on Node.js!

84ea1b1

The SQLite adapter is no longer Bun-only! It now has dual runtime exports:

  • Bun: uses bun:sqlite (unchanged)
  • Node.js: uses node:sqlite with DatabaseSync

The correct implementation is auto-selected based on your runtime — no code changes needed.

@gramio/storage-redis — Bun Native Redis

8190612 98749d1

The Redis adapter now supports Bun's built-in RedisClient alongside ioredis:

ts
// Auto-selected on Bun — no ioredis needed
import { redisStorage } from "@gramio/storage-redis";
const storage = redisStorage({ url: "redis://localhost:6379" });

Dual exports: @gramio/storage-redis (auto-detects runtime), @gramio/storage-redis/ioredis (explicit), @gramio/storage-redis/bun (explicit). The ioredis peer dependency is now optional.

@gramio/test — API Mocking & Chat Simulation

f9b670d 480cde3

onApi / offApi for mocking API responses

ts
import { apiError } from "@gramio/test";

// Static response
env.onApi("sendMessage", { message_id: 1, chat: { id: 1 }, ... });

// Dynamic handler
env.onApi("getChat", (params) => {
    if (params.chat_id === 123) return chatData;
    return apiError(400, "Chat not found");
});

// Simulate errors with retry_after
env.onApi("sendMessage", apiError(429, "Too Many Requests", { retry_after: 30 }));

env.offApi("sendMessage"); // reset single
env.offApi();              // reset all

Chat objects and user interactions

ts
const chat = env.createChat();
const user = env.createUser({ first_name: "Alice" });

await user.join(chat);           // emits chat_member + new_chat_members
await user.sendMessage(chat, "Hello!");  // emits message in chat
await user.click("button_data"); // emits callback_query
await user.leave(chat);          // emits chat_member + left_chat_member

All API calls are recorded in env.apiCalls for assertions.