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.