SaaS Error Monitoring with Telegram

Telegram bots are super fun and easy to set up. In this post, we'll show you how to use Telegram to monitor your SaaS and get notified when errors occur in Makerkit

Getting notified quickly when errors occur in your production application is crucial for maintaining a good user experience. Did you know Telegram offers a simple and free way to get instant notifications right on your phone?

While there are many error monitoring solutions available, and Makerkit implements a few of them, Telegram is a fun and quick way to get notified when errors occur in your application.

In this tutorial, we'll create a custom monitoring service for Makerkit that sends errors and events directly to a Telegram chat. You'll get nicely formatted error messages with full stack traces delivered instantly.

Fun, right? Let's get started!

Prerequisites

Before we start, you'll need:

  1. A Telegram account
  2. Your Makerkit Next.js application up and running
  3. About 10 minutes of your time ⏰

Creating a Telegram Bot

First, let's create a Telegram bot that will send us the notifications:

  1. Open Telegram and search for "@BotFather"
  2. Start a chat and send /newbot
  3. Follow the prompts to create your bot
  4. Save the HTTP API token BotFather gives you - we'll need this later

For example, I named my bot makerkit_monitoring_bot.

Getting Your Chat ID

We need to retrieve the chat ID of the bot.

Now, grab the HTTP API token from BotFather and add it to the URL:

https://api.telegram.org/bot<BOT_TOKEN>/getUpdates

Replace <BOT_TOKEN> with the token you got from BotFather, and open a new browser tab to navigate to the above URL.

You will see a JSON response that looks like this:

{
"ok": true,
"result": [
{
"update_id": 1,
"message": {
"message_id": 1,
"from": {
"id": 123456789,
"is_bot": false,
"first_name": "John",
"last_name": "Doe",
"username": "johndoe",
"language_code": "en"
},
"chat": {
"id": 123456789,
"first_name": "John",
"last_name": "Doe",
"username": "johndoe",
"type": "private"
},
"date": 123456789,
"text": "/start"
}
}
]
}

THe Chat ID is 123456789 from chat.id, which we will need to add to our monitoring service.

Now we add the following env variables to an environment file .env.local:

apps/web/.env.local
NEXT_PUBLIC_MONITORING_PROVIDER=telegram
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_chat_id_here

If you don't have this file yet, create it.

Creating the Monitoring Service

Let's create a new monitoring service package. In your Makerkit project:

  1. Create a new package packages/monitoring/telegram
  2. Create the main service file:

1. Creating a package

We can quickly scaffold a new package using the turbo gen command:

turbo gen package

You can name the package telegram. The package will be created at packages/telegram.

Let's install the package @kit/monitoring-core in the @kit/telegram package:

pnpm add "@kit/monitoring-core@workspace:*" "@types/node" --filter "@kit/telegram -D

2. Creating the main service file

Let's create the main service file packages/telegram/src/telegram.service.ts:

packages/telegram/src/telegram.service.ts
import { MonitoringService } from '@kit/monitoring-core';
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
const CHAT_ID = process.env.TELEGRAM_CHAT_ID!;
export function createTelegramService() {
return new TelegramMonitoringService();
}
class TelegramMonitoringService implements MonitoringService {
private readonly apiUrl: string;
constructor() {
this.apiUrl = `https://api.telegram.org/bot${BOT_TOKEN}`;
}
async captureException<Extra extends object>(
error: Error & { digest?: string },
extra?: Extra,
) {
const message = this.formatError(error, extra);
await this.sendMessage(message);
}
async captureEvent<Extra extends object>(event: string, extra?: Extra) {
const message = this.formatEvent(event, extra);
await this.sendMessage(message);
}
async identifyUser<Info extends { id: string }>(info: Info) {
const message = `👤 User identified:\n${JSON.stringify(info, null, 2)}`;
await this.sendMessage(message);
}
async ready() {
// await this.sendMessage('🚀 Monitoring service connected');
}
private async sendMessage(text: string) {
const payload = {
chat_id: CHAT_ID,
text: this.addEnvironmentInfo(text),
parse_mode: 'markdown',
};
console.log(JSON.stringify(payload, null, 2));
const response = await fetch(`${this.apiUrl}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error(response);
throw new Error(`Telegram API error: ${response.statusText}`);
}
}
private formatError(error: Error, extra?: object) {
return (
`*❌ Error*\n\n` +
`*Message:* \`${error.message}\`\n` +
(extra
? `\n*Extra:*\n\`\`\`\n${JSON.stringify(extra, null, 2)}\`\`\``
: '')
);
}
private formatEvent(event: string, extra?: object) {
return (
`📝 Event: ${event}\n` +
(extra ? `${JSON.stringify(extra, null, 2)}` : '')
);
}
private addEnvironmentInfo(message: string) {
const env = process.env.NODE_ENV ?? 'development';
return `🔷 Environment: ${env}\n\n${message}`;
}
}

Configuration

Add the following environment variables to your .env file:

NEXT_PUBLIC_MONITORING_PROVIDER=telegram
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_chat_id_here

Let's export the service from the entrypoint index.ts:

packages/telegram/src/index.ts
export * from './telegram.service';

This allows us to import the service from @kit/telegram and the configuration from @kit/telegram/config.

Installing the Telegram package

Now we need to install the Telegram package in the main monitoring package (that the main app uses as a proxy to the Telegram service):

pnpm --filter "@kit/monitoring" add "@kit/telegram@workspace:*" -D

This command installs the Telegram package as a dependency in the main monitoring package. In this way, we can register the Telegram service and use it in the main monitoring package.

Registering the Service

Register the monitoring service in your application:

Add Telegram to the enum:

packages/monitoring/core/src/monitoring-providers.enum.ts
export enum InstrumentationProvider {
Baselime = 'baselime',
Sentry = 'sentry',
Telegram = 'telegram',
}

Now, register the Telegram service:

packages/monitoring/api/src/services/get-server-monitoring-service.ts
import { ConsoleMonitoringService } from '@kit/monitoring-core';
import { getMonitoringProvider } from '../get-monitoring-provider';
import { InstrumentationProvider } from '../monitoring-providers.enum';
const MONITORING_PROVIDER = getMonitoringProvider();
/**
* @name getServerMonitoringService
* @description Get the monitoring service based on the MONITORING_PROVIDER environment variable.
*/
export async function getServerMonitoringService() {
if (!MONITORING_PROVIDER) {
console.info(
`No instrumentation provider specified. Returning console service...`,
);
return new ConsoleMonitoringService();
}
switch (MONITORING_PROVIDER) {
case InstrumentationProvider.Telegram: {
const { createTelegramService } = await import('@kit/telegram');
return createTelegramService();
}
case InstrumentationProvider.Baselime: {
const { BaselimeServerMonitoringService } = await import(
'@kit/baselime/server'
);
return new BaselimeServerMonitoringService();
}
case InstrumentationProvider.Sentry: {
const { SentryMonitoringService } = await import('@kit/sentry');
return new SentryMonitoringService();
}
default: {
throw new Error(
`Please set the MONITORING_PROVIDER environment variable to register the monitoring instrumentation provider.`,
);
}
}
}

Prevent instrumentation from erroring out by simply adding a case for Telegram:

packages/monitoring/api/src/instrumentation.ts
case InstrumentationProvider.Telegram: {
return;
}

Testing It Out

Let's test our new monitoring service:

// Anywhere in your app
const monitoring = createMonitoringService();
try {
throw new Error('Test error!');
} catch (error) {
monitoring.captureException(error, {
userId: '123',
context: 'Testing monitoring'
});
}

You should receive a nicely formatted message in your Telegram chat that looks like this:

🔷 Environment: development
❌ Error:
Message: Test error!

What's Next?

You now have a working error monitoring system that will notify you instantly when errors occur in your application! Some ideas for enhancement:

  1. Add filters to ignore certain types of errors
  2. Create different chat groups for different error severities
  3. Add custom formatting for specific types of errors
  4. Add the stack from the error to the message (requires handling formatting that Telegram supports!)

Throw an error from your application

Let's go in the root layout and throw and error before rendering the component:

apps/web/app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Throw an error before rendering the component
throw new Error(`Testing the Telegram service`);
// code...
return (
// code...
);
}

Now run the application and you should see the error message in your Telegram chat.

Conclusion

Adding Telegram error monitoring to your Makerkit application is a great way to stay on top of errors as they happen. The immediate notifications and rich formatting make it easy to understand what went wrong and take action quickly.

The best part? It's completely free and takes just a few minutes to set up. No more missing critical errors in your application!

Remember to secure your bot token and be mindful of what information you send through Telegram. While convenient, you may want to avoid sending sensitive data through this channel.