Pular para o conteúdo

Atrito de revisão automatizada

Revisores bot encontram bugs reais — mas as mesmas categorias de bug se repetem entre os PRs. Padrões recorrentes deveriam virar verificações determinísticas, não revisão agêntica.

O Sentry executa três revisores automatizados em seus PRs: sentry[bot] (predição de bugs), sentry-warden[bot] (verificação de correção/segurança) e cursor[bot] (revisão agêntica do Cursor). Combinados, eles postam 23,7% de todos os comentários substantivos de revisão na nossa amostra de alto atrito, e 68,3% dos PRs de alto atrito têm atividade de revisão de bots.

Esses bots não são ruído. Eles encontram bugs reais — erros de tipo do Pydantic, manipuladores de exceção ausentes, divergências de chaves, brechas de segurança. Cada achado é algo que um desenvolvedor precisa ler, avaliar e atender.

Mas aqui está o insight central desta seção:

As mesmas categorias de bug se repetem entre PRs diferentes. Quando o mesmo padrão de revisão agêntica dispara em PR após PR, esse padrão deveria ser promovido de “revisão de LLM cara em cada commit” para “verificação determinística barata em cada commit”.

A revisão agêntica é a ferramenta certa para encontrar um padrão recorrente nas primeiras vezes. É a ferramenta errada para aplicá-lo uma vez que o padrão é conhecido.

MétricaValor
Total de comentários substantivos de revisão de bots139
Total de comentários substantivos de revisão humana448
Participação dos bots na revisão substantiva23,7%
PRs com atividade de revisão de bots41 / 60
Participação de PRs de alto atrito com bots68,3%
Comprimento médio dos comentários de bots1.565 caracteres
BotComentários substantivosPapel
sentry[bot]81Revisor interno de predição de bugs
sentry-warden[bot]50Verificador interno de correção/segurança
cursor[bot]4Revisão agêntica do Cursor (a maioria filtrada como templates)
github-advanced-security[bot]3Consultor de segurança do GitHub
getsentry-bot1Automação interna

sentry[bot] e sentry-warden[bot] juntos respondem por 94% das revisões substantivas de bots.

Quando lemos os achados reais dos bots, eles se agrupam em um pequeno número de categorias recorrentes. Os agrupamentos abaixo foram identificados lendo os 139 comentários substantivos de bots da nossa amostra. Cada agrupamento inclui o padrão, exemplos e o tipo de verificação determinística que substituiria a revisão agêntica para aquele padrão.

Padrão 1: tratamento de exceção ausente em fetches de modelo

Seção intitulada “Padrão 1: tratamento de exceção ausente em fetches de modelo”

QuerySubscription.DoesNotExist not handled in get_metric_issue_contextMetricIssueContext.from_group_event() calls QuerySubscription.objects.get(id=...) without handling DoesNotExist. If the subscription is deleted between alert creation and notification rendering, this will crash. — sentry-warden[bot] em #111674

get_from_cache raises ValueError because DetectorGroup has no cache_fields defined — The base BaseManager has empty cache_fields by default, so get_from_cache will raise. — sentry-warden[bot] em #111714

If project_id is None in trigger_coding_agent_launch, Project.objects.get_from_cache fails silentlysentry[bot] em #111691

Substituir por: uma regra customizada de Ruff/Semgrep que sinalize chamadas Model.objects.get(...) e Model.objects.get_from_cache(...) dentro de funções que não têm um except Model.DoesNotExist: correspondente (ou um try envolvente). Para get_from_cache, verificar também se o modelo define cache_fields. É uma regra de 30 linhas que captura o padrão para sempre.

Padrão 2: .filter().first() com except DoesNotExist inalcançável

Seção intitulada “Padrão 2: .filter().first() com except DoesNotExist inalcançável”

AttributeError on None when Group is not found — The function uses .filter().first() which returns None when no record exists, but the except Group.DoesNotExist handler is unreachable because .filter().first() never raises exceptions. — sentry-warden[bot] em #111724

Substituir por: um lint de AST trivial: se um bloco try contém .filter(...).first() e a cláusula except captura *.DoesNotExist, sinalizar como inalcançável. Esse padrão é mecânico — nenhum LLM necessário.

Padrão 3: acesso direto a dict em respostas de API externas

Seção intitulada “Padrão 3: acesso direto a dict em respostas de API externas”

KeyError when GitHub API response lacks 'installations' key — Line 908 uses direct dict access installed_orgs["installations"] on the GitHub API response. If the API returns an error or malformed response, this will raise KeyError. — sentry-warden[bot] em #111728

Substituir por: um ADR que exija que respostas de API externas sejam parseadas por meio de TypedDicts ou modelos Pydantic com validação explícita de campos. Uma regra de lint pode sinalizar acesso direto ["key"] em variáveis que vêm de chamadas requests.get(...).json() ou gh_client.*.

Padrão 4: uso indevido do sistema de tipos em serializers

Seção intitulada “Padrão 4: uso indevido do sistema de tipos em serializers”

TypeError when saving defaultCodingAgent: str | None union type is not callable — The defaultCodingAgent option tuple uses str | None as the type parameter. The code calls type_(data[key]) to convert the value. However, str | None is a types.UnionType which cannot be called as a function. — sentry-warden[bot] em #111697

Pydantic validation error when coding_agent is 'seer' with integration_id — The code creates a SeerAutomationHandoffConfiguration with target='seer'. However, the Pydantic model’s target field is Literal['cursor_background_agent', 'claude_code_agent', ...] which excludes 'seer'. — sentry-warden[bot] em #111697

Substituir por: configuração mais estrita de mypy/pyright no CI, mais uma verificação customizada de que a anotação de tipo passada para o registro de opções seja um tipo callable concreto, não um UnionType. A divergência de Literal do Pydantic é capturada pelo mypy se o modo strict estiver habilitado no módulo relevante.

Padrão 5: divergência de chave/identificador entre escritor e leitor

Seção intitulada “Padrão 5: divergência de chave/identificador entre escritor e leitor”

Organization option key mismatch causes setting to have no effect — The new option registers with key sentry:default_automated_run_stopping_point but the code that consumes this value reads from key sentry:default_stopping_point. Any value set via the API will be stored but never read. — sentry-warden[bot] em #111697

Substituir por: este é um dos achados de bot mais valiosos — e um dos mais fáceis de converter em uma verificação. O Sentry já tem um registro de opções de organização. Um fixture de teste pode afirmar que toda chave escrita via OrganizationOption.objects.set_value(...) corresponde a uma chave registrada, e que todo ponto de leitura (get_value(...)) usa uma chave que existe no registro. Bônus: extrair as chaves de opção para um enum tipado de modo que o sistema de tipos capture o erro de digitação em tempo de edição.

Padrão 6: blocos if/elif não exaustivos causando UnboundLocalError

Seção intitulada “Padrão 6: blocos if/elif não exaustivos causando UnboundLocalError”

The if/elif block checking the data type is not exhaustive. A new subclass of BaseMetricAlertNotificationData would cause an UnboundLocalError because metric_issue_context would not be assigned.sentry[bot] em #111674

Substituir por: usar pattern matching estrutural do Python 3.10+ com exaustividade explícita case _:, OU usar o padrão assert_never() do mypy para uniões tagueadas. Ambos são detectáveis por análise estática uma vez que a convenção esteja em vigor.

Padrão 7: atualizações de listas companheiras esquecidas

Seção intitulada “Padrão 7: atualizações de listas companheiras esquecidas”

The python-litestar platform is missing from several product onboarding arrays in platformCategories.tsx, which will prevent its onboarding documentation from loading for Logs, Metrics, Profiling, and Replay. — sentry[bot] em #111522

The python-litestar platform is missing from the Python platforms set in monitorQuickStartGuide.tsx, causing it to be miscategorized in the Crons monitor form. — sentry[bot] em #111522

Em um único PR (#111522), o bot encontrou o mesmo tipo de bug quatro vezes separadas — adicionar uma plataforma a uma lista e esquecer as outras listas.

Substituir por: refatorar o registro de plataformas em uma única fonte de verdade que derive todas as listas companheiras de um registro tipado. É uma refatoração única que elimina uma classe inteira de bugs. Como alternativa, uma verificação de consistência no estilo CODEOWNERS: quando um PR modifica platformCategories.tsx, verificar se qualquer novo identificador de plataforma aparece em todos os arrays relevantes.

Padrão 8: tags de métricas de alta cardinalidade

Seção intitulada “Padrão 8: tags de métricas de alta cardinalidade”

The circuit breaker for Sentry App webhooks uses a per-app slug for its metrics key, creating high-cardinality data because a metrics_key override is not provided.sentry[bot] em #111723

Substituir por: uma regra de lint que sinalize metrics.incr(name, tags={"slug": ...}) (ou padrões similares de alta cardinalidade) sem um override explícito de metrics_key ou um comentário de allowlist. Esse é um antipadrão conhecido com uma correção conhecida.

The form’s saving state is not reset in the catch block on submission failure, leaving the submit button permanently busy and preventing retries.sentry[bot] em #111490

Substituir por: uma regra de ESLint (ou convenção de custom hook do React) que exija try/finally para qualquer setter de estado dentro de um handler de submissão assíncrono — setSaving(true) no try, setSaving(false) no finally. A convenção previne completamente o bug do caminho de erro.

Padrão 10: verificações de truthiness removidas → None salvo como valor explícito

Seção intitulada “Padrão 10: verificações de truthiness removidas → None salvo como valor explícito”

The removal of the truthiness check for stopping_point causes None to be explicitly saved as a project option, overriding the intended default value of "code_changes".sentry[bot] em #111697

Substituir por: um ADR (e uma regra de lint) que diga que setters de opção devem rejeitar None a menos que o schema da opção explicitamente o permita. Combinado com o Padrão 4 (registro tipado de opções), isso se torna uma verificação estática.

PadrãoPRs na amostra com correspondênciaBot(s) reportandoCusto da verificação determinística
Tratamento de DoesNotExist ausente3+warden, sentry-botBaixo — regra de Ruff/Semgrep
.filter().first() + except inalcançável1+wardenTrivial — lint de AST
Acesso direto a dict em respostas de API2+wardenMédio — adoção de TypedDict + lint
UnionType não callable1wardenBaixo — modo strict do mypy
Divergência de Literal do Pydantic1wardenTrivial — modo strict do mypy
Divergência de chave de opção (escritor/leitor)1wardenBaixo — registro tipado de opções
if/elif não exaustivo1sentry-botMédio — convenção assert_never
Lista companheira esquecida (registro de plataformas)1 (4 instâncias)sentry-botMédio — refatorar para fonte única
Tag de métrica de alta cardinalidade1sentry-botBaixo — regra de lint
Estado de formulário não reiniciado em erro1sentry-botTrivial — regra de ESLint
None salvo como valor explícito1sentry-botBaixo — validação de schema

Esses 10 padrões sozinhos respondem pela maioria dos achados substantivos de bots na nossa amostra. Cada um tem um substituto de verificação determinística que custa muito menos do que a revisão agêntica por PR.

A revisão agêntica custa aproximadamente:

  • Inferência de LLM: um comentário de revisão de bot de 1.500 caracteres implica centenas de milhares de tokens de entrada (o diff + contexto) e uma saída relevante. Pela precificação atual dos modelos, isso é da ordem de dezenas de centavos por achado.
  • Atenção do desenvolvedor: achados de 1.565 caracteres levam de 30 a 60 segundos para ler e avaliar. Multiplicado pelos 139 achados em 60 PRs, isso são cerca de 70-140 minutos de atenção cumulativa do desenvolvedor apenas na nossa amostra. Extrapolado para o volume total de PRs do Sentry, o investimento de tempo é substancial.
  • Latência: a revisão agêntica roda depois que o PR é aberto. Um bug encontrado após a abertura é mais caro do que um bug capturado em tempo de edição por um type checker ou regra de lint.

Uma verificação determinística custa:

  • Tempo de execução de CI: microssegundos a milissegundos por arquivo.
  • Atenção do desenvolvedor em tempo de edição: os erros de lint aparecem na IDE antes mesmo de o PR ser aberto. Capturar o bug em tempo de edição é cerca de 100x mais barato do que capturá-lo na revisão de PR.
  • Custo zero de inferência de LLM por PR.

O argumento econômico e ergonômico é claro: todo achado de bot que se repete é candidato à promoção para uma verificação determinística.

A revisão agêntica ainda tem um papel legítimo:

  1. Descoberta de padrões pela primeira vez — encontrar formatos novos de bugs que ainda não correspondem a nenhuma regra conhecida
  2. Raciocínio entre arquivos — bugs que abrangem múltiplos arquivos de maneiras que regras no nível de AST não conseguem capturar facilmente
  3. Correção semântica — achados de “este código faz a coisa errada” que exigem entender a intenção
  4. Revisão de segurança para superfícies de ataque novas — onde o modelo mental de um atacante é necessário

O objetivo não é eliminar a revisão de bots. O objetivo é promover continuamente achados da revisão agêntica para regras determinísticas, de modo que a atenção do bot fique livre para os casos genuinamente novos.

Este insight remodela GEN-003 (Converter achados recorrentes de bots em regras determinísticas) — em vez de um “protocolo de triagem” para achados de bots, o ADR certo é um protocolo de promoção: um fluxo de trabalho para converter achados recorrentes de bots em regras de lint, verificações de tipo ou validações de schema.

Ele também fortalece vários outros ADRs:

  • BE-001 (Evolução do contrato de API) — muitos achados de bots (divergência de chave de opção, propagação de None, divergências de Literal do Pydantic) são detectáveis se o contrato de API for tipado e orientado por registro
  • GEN-002 (Matriz de evidência de testes) — os achados de teste dos bots (divergências de asserção, casos extremos ausentes) viram templates de scaffold de teste
  • FE-001 (Convenções de frontend) — o achado de bot de estado de formulário não reiniciado vira uma convenção do React, não um comentário de revisão ad hoc
  • Viés de amostra: esta análise é baseada nos 60 PRs de maior atrito, não em todos os 500. A atividade de bots em PRs de baixo atrito pode diferir.
  • As contagens de padrões são mínimos: a coluna “PRs na amostra com correspondência” mostra ocorrências na nossa amostra aprofundada de 60 PRs. No volume total de PRs do Sentry, cada padrão provavelmente recorre com muito mais frequência. Uma análise formal de recorrência exigiria taguear os achados de bots sistematicamente por padrão.
  • Filtro de substantivo vs. ruído: excluímos comentários de template (resumos BUGBOT_REVIEW, BACKEND_TEST_FAILURES), mas contamos as revisões substantivas de bots no nível da linha.
  • Os bots estão evoluindo: os bots internos do Sentry (sentry[bot], sentry-warden[bot]) são relativamente novos. À medida que mais padrões recorrentes forem promovidos para verificações determinísticas, os achados dos bots devem se deslocar para casos genuinamente novos.