Um único erro do ChatGPT causou uma perda de receita de mais de 15 milhões de won
(asim.bearblog.dev)- Uma startup que tinha acabado de ativar a monetização enfrentou uma falha no pagamento de assinaturas, mas como o problema não era reproduzido internamente, a identificação da causa foi atrasada em 5 dias
- A falha começou ao copiar o formato de conversão Prisma/TypeScript→Python/SQLAlchemy gerado pelo ChatGPT, em que, no lugar de uma função de geração de UUID, entrou uma string de ID hardcoded como se fosse o valor padrão
- Por causa da estrutura com 8 tarefas no AWS ECS e 5 instâncias em cada uma, os usuários podiam acabar encontrando um entre 40 pools de IDs únicos diferentes, e durante o dia o problema ficava mascarado por implantações frequentes
- À noite, quando as implantações paravam, o único ID de cada servidor se esgotava, e depois disso novas tentativas de assinatura falhavam por conflito de ID único
- Com base em 50 reclamações por dia, ao longo de 5 dias, e em uma assinatura mensal de $40, a perda foi estimada em $10.000 por mês, e a ausência de testes, logs e alertas, além da cópia de código, ampliou o impacto do incidente
Falha na assinatura exposta logo após a monetização
- A startup ativou a monetização pela primeira vez em maio e conseguiu o primeiro cliente em menos de 1 hora após o lançamento
- Na manhã do dia seguinte, havia mais de 40 reclamações de usuários acumuladas no Gmail
- Os usuários não conseguiam concluir a assinatura
- Eles relatavam que, ao clicar no botão de assinatura, aparecia um spinner de carregamento infinito
- Foi criado um novo usuário para testar diretamente, mas internamente a assinatura funcionava normalmente, então não foi possível reproduzir a causa
- Durante o horário de trabalho quase não havia reclamações, e a falha se acumulava principalmente durante a noite
Implementação da monetização sob pressão de tempo
- Maio foi o momento em que a turma YC S23 começou, e a equipe ainda não tinha certeza de qual direção seria a melhor após o lançamento
- O group partner da YC, Dalton, aconselhou usar assinantes pagantes como métrica para definir a direção e dobrar o preço mensal inicialmente imaginado
- O preço final foi definido em $40 por mês
- O projeto era originalmente full-stack em NextJS, mas durante o trabalho de monetização estava sendo migrado para Python/FastAPI
- O ChatGPT foi usado no processo de migração
- A integração com Stripe também foi concluída
- Nos 5 dias seguintes, o sono foi drasticamente reduzido, e foi preciso lidar com 30 a 50 e-mails de reclamação por dia
O formato de conversão de modelos criado pelo ChatGPT
- Durante a migração do backend, os modelos de banco de dados foram movidos de Prisma/TypeScript para Python/SQLAlchemy
- A conversão dos modelos era tediosa, e como pareceu que o ChatGPT fazia isso bem, ele foi usado em quase toda a migração
- O código gerado foi copiado e colado, depois testado para ver se funcionava, e como parecia normal em produção, seguiram assim
- Na época, as inserções no banco de dados ainda eram feitas pela API em Next, e o backend em Python apenas lia o banco
- Ao implementar a funcionalidade de assinatura, começou-se pela primeira vez a inserir registros no banco a partir do Python
- Os novos modelos SQLAlchemy foram criados manualmente, mas o formato gerado pelo ChatGPT nos modelos existentes foi copiado diretamente
- O mesmo problema foi introduzido na forma de geração de ID de todos os modelos
A causa real e por que não aparecia durante o dia
- O erro central foi passar uma única string de ID hardcoded, em vez de passar uma função ou lambda que gerasse UUIDs
- Quando um usuário concluía a assinatura com aquele ID em uma instância específica do backend, as tentativas seguintes de assinatura na mesma instância geravam conflito de ID único
- A configuração do backend escondeu esse problema por mais tempo
- Eram operadas 8 tarefas no ECS da AWS
- Cada tarefa executava 5 instâncias do backend
- Os usuários podiam potencialmente chegar a um entre 40 IDs diferentes
- Durante o dia, eram feitos de 10 a 20 commits diretos na branch main por dia, e cada um deles disparava uma nova implantação do backend
- A cada implantação, surgiam 40 novos IDs que os clientes podiam usar
- À noite, os commits e as implantações paravam, e o único ID de cada servidor era rapidamente consumido
- No início, havia quase 40 servidores capazes de aceitar assinaturas, mas com o tempo isso se aproximava de 0
Escala da perda e medidas posteriores
- A perda foi estimada em $10.000 de receita mensal com o cálculo
50 emails/day x 5 days x $40/month- Esse cálculo considera apenas os usuários que enviaram reclamações
- Foram necessários 5 dias para encontrar a causa, inúmeros e-mails, centenas de logs no Sentry, uma longa conversa no Discord com engenheiros da Stripe e a revisão de cinco arquivos principais
- Depois de encontrar a causa, Adam publicou rapidamente a correção
- Depois disso, foram adicionados testes unitários/de integração robustos, alertas e logs
- O caso mostra que, quando erro humano, falta de testes, cópia de código e push direto na main se combinam, até uma pequena linha pode levar a uma grande perda de receita
2 comentários
Ué, código gerado automaticamente por IA tem que ser revisado, né? Por que usar isso do jeito que saiu?
Opiniões no Hacker News
A falta de monitoramento foi o que fez US$ 10 mil irem pelo ralo. O app estava gerando exceções de banco de dados continuamente, em grande volume, e ninguém recebeu alerta
Se houvesse um alerta desses, teria sido uma investigação de 5 minutos, não de 5 dias. Se eles não corrigiram o sistema de alertas, na prática não corrigiram nada
Programar é fácil quando tudo está funcionando bem; a parte difícil é lidar com os problemas
A partir do momento em que entram clientes pagantes, é preciso ter alguém com conhecimento e experiência para lidar com logging, monitoramento, alertas, segurança etc. Não dá para tratar DevOps de forma amadora
Mas a parte realmente absurda é não haver registro de erros e alertas no banco de dados. Isso não é um código legado de 20 anos, é um produto novo, e também não é código da época em que erros de DB eram usados como validação de dados
O post do blog está dando 404, então deixo o link do Web Archive
https://web.archive.org/web/20240610032818/https://asim.bear...
O autor acrescentou uma correção importante: as práticas ali eram muito ruins e constrangedoras, e depois disso eles adicionaram testes unitários/de integração robustos e alertas/logging. No fim, foi erro humano e, em retrospecto, algo claramente evitável
Ele também acrescentou que isso aconteceu nas primeiras semanas da empresa, sob grande pressão de tempo, e pediu que fosse visto como uma história engraçada sobre a reprodutibilidade peculiar de um bug em produção
Foi um erro tolo, mas seres humanos, individualmente ou em grupo, inevitavelmente cometem erros tolos
https://0912i390129ionkjan.bearblog.dev/how-a-single-chatgpt...
https://webcache.googleusercontent.com/search?q=cache%3Ahttp...
O erro ficou visível de imediato. Com todo respeito pela equipe, isso não tem muito a ver com o ChatGPT, e sim com o uso de um modelo de programação com o qual a equipe não estava suficientemente familiarizada
Mesmo que tivesse passado por code review, é bem provável que tivesse sido pego com ferramentas de monitoramento que levam 5 minutos para configurar
Um título como “Cometemos um erro de programação ao usar um LLM e, por falta de garantia de qualidade, isso nos custou 10 mil dólares” não geraria a reação de executivos do tipo “quanto é nossa exposição se o ChatGPT estragar tudo?”. Vai ter um monte de gerentes de médio e alto escalão postando esse texto no LinkedIn
Um LLM não pode “cometer erros”. Ele não é determinístico, não infere, não pensa nem executa lógica. É um gerador extremamente sofisticado de salada de palavras que usa probabilidades estatísticas, e não há garantia de que o que ele gera esteja certo ou seja preciso; portanto, por definição, chamar isso de erro também não é adequado
Edit: depois de o post ser muito downvotado por motivos óbvios, a posição dele subiu de repente, o que parece significar que um moderador o impulsionou: https://hnrankings.info/40627558/
É engraçado que um post clickbait a ponto de, pelas regras, o título ter que ser alterado tenha sido impulsionado por um moderador. E, além disso, o fato de o autor aparentemente trabalhar em uma empresa da Y Combinator deve ser uma coincidência total: https://news.ycombinator.com/item?id=40629998
Disse que, para gerar o UUID da chave primária, não se deve usar
str(uuid.uuid4()), mas sim passar diretamente o chamáveluuid.uuid4, e que o SQLAlchemy chama a função ao criar o valor. Também disse que o valor padrão de dataserver_default=text("(now())")talvez não funcione como esperado e recomendou usarfunc.now(); pediu para verificar também os imports deuuide detextdo SQLAlchemy, e sugeriu considerarDateTime(timezone=True)para lidar com fuso horárioDepois, como código corrigido, sugeriu
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), unique=True, nullable=False), e a adição delambda:ali corrigiu o problemauuid.uuid4()como uma definição de schema em algo como Prisma. Então o bug em si não surpreende, e eu poderia ter cometido o mesmo erroAinda assim, um único
kubectl logsteria pego isso na hora. E, além disso: sair de Next.js e Prisma para Python? Por quê?O erro em si é compreensível. Parece algo relativamente fácil de deixar passar mesmo escrevendo código sem o ChatGPT
Mas não entendo por que não foi identificado depois da primeira falha. Essa empresa não tinha logging? O fato de o backend estar tentando reutilizar um UUID deveria ter ficado evidente assim que se olhasse o erro
Passar por engano uma string para uma função que espera um objeto chamável em vez de
Stringé algo comum. Se não estivessem usando ORM, acho que esse problema específico teria sido evitado, mas talvez isso seja só meu preconceito pessoal contra ORMs. Bugs parecidos podem muito bem acontecer também fora do contexto de bancos de dadosAs pessoas que afirmam com tanta certeza que teriam pego esse bug ou são engenheiros muito melhores do que eu, ou, mais provavelmente, têm uma percepção um pouco equivocada das próprias habilidades
Dito isso, não ter logs ou não olhar os logs é realmente difícil de entender. Se era ECS, eu esperaria que a exceção de Duplicate Key fosse propagada para o CloudWatch sem configuração extra; fico curioso se isso não aconteceu, ou se aconteceu e mesmo assim ninguém verificou durante a noite quais exceções tinham ocorrido
Em situações assim, é útil perguntar por que a detecção demorou e por que o diagnóstico levou tanto tempo
Já vi o mesmo erro várias vezes em código feito por pessoas. Especialmente em React / TypeScript / JavaScript, é comum alguém esquecer uma lambda
O post do blog parece não explicar direito a causa raiz do problema e pula direto para culpar o ChatGPT. Quando se trabalha com pressa e se coloca na main um commit com uma mudança grande ou sem revisão de colegas, esse tipo de coisa acontece
O problema real é que, quando se tem pressa, se pega atalhos e não há testes suficientes nem code review por pares, erros aparecem. Acho que bastaria ter um teste tentando várias opções de assinatura para encontrar isso imediatamente
Se você coloca alguém assim perto de código financeiramente importante, problemas parecidos vão surgir, e eu passaria a questionar o julgamento de quem decidiu colocar esse código em produção com quase nenhum teste
É surpreendente que não houvesse uma regra de lint para esse caso
Espero que não tenha sido assim
A parte “o projeto original era full-stack NextJS, mas eu queria primeiro migrar tudo para Python/FastAPI” me fez arregalar os olhos
Não sei como uma startup sem clientes justifica uma reescrita
Com ou sem clientes, não entendo por que fazer, num estágio tão inicial, o que é essencialmente um deslocamento horizontal de Node para Python. Se tivessem centenas de clientes e estivessem mudando para algo como Go, eu até poderia entender um pouco, mas ainda assim seria questionável
Por exemplo, é preciso criar um monte de objetos DTO, mas o AutoMapper não funciona com a combinação de versões e a configuração do projeto que eu uso, e o Entity Framework mais a serialização/desserialização JSON trazem mais dor do que benefício
Claro que dá para resolver isso gradualmente. Dá para mergulhar na documentação, misturar alguns hacks, atualizar pacotes e reescrever configurações. Mas, sendo humano, dá vontade de pegar um galão de gasolina metafórico, queimar tudo e fazer o segundo sistema melhor. Claro que, na prática, ele não fica melhor; só surgem outros pontos de dor, e talvez ele nem faça tudo que o primeiro sistema fazia, ou não faça direito
No trabalho também sinto o mesmo impulso sempre que vejo um sistema legado ou trabalhoso. É preciso um esforço ativo e contínuo para vencer a parte do cérebro que grita para reescrever tudo. Às vezes uma mudança arquitetural como uma reescrita ou adoção de contêineres dá certo, mas em geral ela leva você para dentro das chamas ou para um trabalho interminável
Exceto quando há alta confiança de que isso vai melhorar a operação do sistema ou a experiência de desenvolvimento de outros desenvolvedores, é melhor não ceder a esse impulso
O ChatGPT, na verdade, foi o lado que gerou o dinheiro que o app ganhou. Sem o ChatGPT, eles não teriam capacidade de implementá-lo
A falta de capacidade em codar, depurar, fazer logging e monitoramento foi o que queimou US$ 10 mil; nessa história, o efeito líquido do ChatGPT é positivo
Todas as mensagens de commit têm emojis. Tem macaco, banana, foguete, fogos de artifício, de tudo
https://grook.ai/share?id=e269e88a7b1a71eff4f176c864b30161&x...
Dizem que originalmente era full-stack NextJS e que, ao migrar o backend para Python/FastAPI, estavam traduzindo o modelo de banco de dados de Prisma/Typescript para Python/SQLAlchemy. O texto diz que esse trabalho era tedioso e, vendo que o ChatGPT fazia isso razoavelmente bem, usaram-no em quase toda a migração
Se o ChatGPT não existisse, talvez nem tivessem tentado essa migração preliminar, então é difícil dizer que o efeito líquido foi positivo. A stack existente talvez tivesse logging de erros melhor, talvez não; e, por ser código escrito por eles mesmos, talvez entendessem melhor a estrutura e precisassem menos disso
A decisão de “reescrever todo o código uma segunda vez” antes de ativar a monetização já é interessante por si só
Há uma frase: “Quero começar dizendo que as práticas aqui foram ruins e poderiam ter sido evitadas. Isso aconteceu em outro período, sob grande pressão de tempo. Leiam levando isso em conta”
É por causa desse tipo de restrição que assinaturas de software me dão medo
Já cobramos um usuário duas vezes por causa de uma condição de corrida. Por isso, quando vejo timeouts ou erros relacionados a dinheiro, fico paranoico a ponto de presumir primeiro que o pagamento foi feito e conferir de novo depois
Código em TypeScript e Python, frameworks como Next.js, 8 tarefas na AWS, cada uma rodando 5 instâncias, e mesmo assim a receita foi de US$ 40, com apenas algumas semanas de desenvolvimento?
Fico pensando que diabos aconteceu. Dizem que corrigiram porque o código estava uma bagunça por causa da restrição de tempo, mas é ainda pior terem gastado tempo com refatoração entre linguagens e com a criação de um sistema distribuído sem motivo nenhum.
É uma complexidade autoinfligida, tentando fazer malabarismo ao mesmo tempo com funcionalidades e uma complexidade técnica absurda. Não faço ideia do que estavam pensando.
Correção: é uma empresa da turma de verão de 2023 da YC, mas no verão de 2024 o produto ainda parece estar atrás de uma lista de espera. Talvez porque estejam reescrevendo em Rust.
Parece que ele nem deve ter escrito 1.000 linhas de Python na vida, mas identificou o problema corretamente.
O Python tem uma falha por não ter copiado direito a estratégia de avaliação do Common Lisp. Se, nas expressões de valor padrão de argumentos opcionais de uma função, houver um argumento como
foo=obj.whatever(),obj.whatever()é avaliado no momento em que a definição da função é processada, não quando a função é chamada.Suspeito que tenham feito isso de propósito por eficiência. O Python tem outra falha: não há uma sintaxe literal de verdade para objetos comuns como listas.
[1, 2, 3]não é um literal, é mais parecido com um construtor; cada vez que é avaliado, precisa criar uma nova lista e preenchê-la com valores.O projetista provavelmente não queria que um parâmetro como
list=[]criasse uma nova lista vazia toda vez que o argumento fosse omitido. Em Lisp,'(1 2 3)e'()são literais de verdade e apontam para o mesmo objeto sempre que são referenciados. O programador pode escolher se quer usar(list 1 2 3)ou'(1 2 3)como expressão de valor padrão.O primeiro cria um novo objeto mutável a cada vez, como
[1, 2, 3]; o segundo quase certamente retorna o mesmo objeto e, de forma confiável e portável, não pode ser modificado. Como as linguagens populares modernas já têm a maior parte dos recursos do Lisp, isso parece uma piada do tipo “não há nada a perder”.foo=obj.whatever(),obj.whatever()é avaliado no momento em que a definição da função é processada, e não quando a função é chamada, não parece poder estar correta.Não sei o que aconteceria se
.whatever()dependesse de um estado interno que muda depois da inicialização do objeto.