Como implementei logs no meu SaaS através do Discord
Introdução
Opa! Quero falar aqui rapidinho sobre como implementei logs de eventos no meu SaaS usando o Discord, e te mostrar um pouco do código, caso você queira fazer também.
Antes de continuar, sei que usar o Discord, de longe, não é um jeito profissional de fazer isso. Mas, honestamente, achei que seria divertido. E, de qualquer maneira, é útil. Por isso, fiz.
Webhooks do Discord
Quando você cria um servidor no Discord, você pode criar vários webhooks conectados com aquele servidor. Cada webhook pode enviar mensagens em um canal específico do servidor, como se fosse um usuário. É bem fácil configurar isso pela UI das configurações do servidor.
Cada um dos webhooks tem uma URL específica, que, quando recebe uma requisição POST
, envia alguma mensagem (definida por quem envia a request) para o canal do servidor para o qual foi configurado.
Com isso, sempre que algum evento acontecer no seu site (digamos, um clique em um botão), você pode disparar um POST
para o webhook, e a mensagem chegará no seu servidor.
Hora de codar
Minha classe para criar logs atualmente é essa:
// Log events to Discord via webhook
import { DiscordEmbed, EmbedColors } from "@/types/logging/discord-types";
import {
CheckoutSessionCreationEvent,
PasswordChangeEvent,
PasswordResetEvent,
PasswordResetRequestEvent,
PurchaseEvent,
} from "@/types/logging/event-types";
interface LoggerConstructor {
passwordChangeWebhookUrl: string;
passwordResetWebhookUrl: string;
purchaseWebhookUrl: string;
checkoutSessionWebhookUrl: string;
}
class Logger {
private passwordChangeWebhookUrl: string;
private passwordResetWebhookUrl: string;
private purchaseWebhookUrl: string;
private isLoggingEnabled: boolean = process.env.IS_LOGGING_ENABLED === "true";
private checkoutSessionWebhookUrl: string;
constructor(params: LoggerConstructor) {
this.passwordChangeWebhookUrl = params.passwordChangeWebhookUrl;
this.passwordResetWebhookUrl = params.passwordResetWebhookUrl;
this.purchaseWebhookUrl = params.purchaseWebhookUrl;
this.checkoutSessionWebhookUrl = params.checkoutSessionWebhookUrl;
}
async logPasswordChange(event: PasswordChangeEvent) {
if (!this.isLoggingEnabled) return;
const embed: DiscordEmbed = {
title: "An user has changed their password",
description: `User with email ${event.userEmail} has changed their password.`,
footer: {
text: `Timestamp: ${event.timestamp}`,
},
color: EmbedColors.SUCCESS,
};
await fetch(`${this.passwordChangeWebhookUrl}?wait=true`, {
method: "POST",
body: JSON.stringify({ embeds: [embed] }),
headers: {
"Content-Type": "application/json",
},
});
}
async logPasswordReset(event: PasswordResetEvent) {
if (!this.isLoggingEnabled) return;
const embed: DiscordEmbed = {
title: "An user has reset their password",
description: `User with email ${event.userEmail} has reset their password.`,
footer: {
text: `Timestamp: ${event.timestamp}`,
},
color: EmbedColors.SUCCESS,
};
await fetch(`${this.passwordResetWebhookUrl}?wait=true`, {
method: "POST",
body: JSON.stringify({ embeds: [embed] }),
headers: {
"Content-Type": "application/json",
},
});
}
async logPasswordResetRequest(event: PasswordResetRequestEvent) {
if (!this.isLoggingEnabled) return;
const embed: DiscordEmbed = {
title: "A user has requested a password reset",
description: `User with email ${event.userEmail} has requested a password reset. An e-mail was sent to them containing the password reset link.`,
footer: {
text: `Timestamp: ${event.timestamp}`,
},
color: EmbedColors.SUCCESS,
};
await fetch(`${this.passwordResetWebhookUrl}?wait=true`, {
method: "POST",
body: JSON.stringify({ embeds: [embed] }),
headers: {
"Content-Type": "application/json",
},
});
}
async logPurchase(event: PurchaseEvent) {
if (!this.isLoggingEnabled) return;
const embed: DiscordEmbed = {
title: "A user has purchased a plan",
description: `User with email ${event.customerEmail} has purchased the ${event.plan} plan.\nPayment gateway: ${event.paymentGateway}`,
footer: {
text: `Timestamp: ${event.timestamp}`,
},
color: EmbedColors.SUCCESS,
};
await fetch(`${this.purchaseWebhookUrl}?wait=true`, {
method: "POST",
body: JSON.stringify({ embeds: [embed] }),
headers: {
"Content-Type": "application/json",
},
});
}
async logCheckoutSessionCreation(event: CheckoutSessionCreationEvent) {
if (!this.isLoggingEnabled) return;
const embed: DiscordEmbed = {
title: "A checkout session has been created",
description: `User with email ${event.customerEmail} has created a checkout session for the ${event.plan} plan.\nPayment gateway: ${event.paymentGateway}`,
footer: {
text: `Timestamp: ${event.timestamp}`,
},
color: EmbedColors.SUCCESS,
};
await fetch(`${this.checkoutSessionWebhookUrl}?wait=true`, {
method: "POST",
body: JSON.stringify({ embeds: [embed] }),
headers: {
"Content-Type": "application/json",
},
});
}
}
export const logger = new Logger({
passwordChangeWebhookUrl: process.env.PASSWORD_CHANGE_WEBHOOK_URL,
passwordResetWebhookUrl: process.env.PASSWORD_RESET_WEBHOOK_URL,
purchaseWebhookUrl: process.env.PURCHASE_WEBHOOK_URL,
checkoutSessionWebhookUrl: process.env.CHECKOUT_SESSION_WEBHOOK_URL,
});
Pontos importantes:
Criei o tipo DiscordEmbed
com base na própria documentação do Discord sobre webhooks.
Para usar em algum lugar do meu código, tudo que preciso fazer é algo como:
import { logger } from "@/logger.ts" // importando instância da classe Logger
await logger.logCheckoutSessionCreation({
customerEmail: params.customerEmail,
plan:
params.priceId === config.stripe.plans.basic.priceId
? "BASIC"
: "COMPLETE",
paymentGateway: "STRIPE",
timestamp: new Date().toLocaleString("pt-BR"),
});
Quando crio um checkout e vou no canal que configurei o webhook, vejo a seguinte mensagem:
Finalizando
Então, com esses webhooks configurados, finalmente posso ficar mais ansioso para receber notificações sobre pessoas que compraram o meu SaaS. Maravilha, cara!
Se tiver alguma dúvida sobre como implementar, pode me perguntar aqui.
É isso, valeu pela atenção. Até mais!