Executando verificação de segurança...
11

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:
Mensagem do webhook

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!

Carregando publicação patrocinada...
2

Que legal, cara! Dado o contexto faz muito sentido. Um SaaS para servidores do Discord, pede um tooling baseado no Discord.

Me lembra aquelas integrações de plataformas de monitoramento com Slack, por exemplo.

Parabéns!

2

Valeu!!

Realmente, faz bastante sentido, dado o contexto. Como eu falei, não é o jeito mais profissional de fazer, mas é um jeito bem simples e fácil de ficar por dentro do que tá rolando no seu SaaS.

1

Me impressiona a coragem de colocar acesso para sempre a um pagamento unico que permite acesso vitalicio ao seu sistema. Legalmente, se eu prometo acesso para sempre, eu nunca mais poderei interromper o serviço ao cliente ou ele poderá me cobrar na lei caso eu o faça. Não poderei simplesmente retirar o sistema do ar quando ver que a estrategia de cobrança fez meu negocio ruir. Vendendo a um valor tão baixo, praticamente o valor de 5 min de programação, conforme aumenta a quantidade de utilizadores e o custos com infra e tecnologia e suporte começam a explodir e o valor de entradas começa a baixar, pois não existe uma recorrencia, o negocio começa a ruir. A primeira regra do SaaS é, jamais cobre preço unico oferecendo acesso vitalicio. A não ser que seja uma solução embarcada e não dependa de acesso constante a sua infra, o que não é o caso de saas.