Skip to content

release: corte de agosto — develop para main (40 commits) - #123

Merged
gomessguii merged 40 commits into
mainfrom
develop
Sep 2, 2026
Merged

release: corte de agosto — develop para main (40 commits)#123
gomessguii merged 40 commits into
mainfrom
develop

Conversation

@gomessguii

Copy link
Copy Markdown
Member

Corte de release: leva a develop para a main40 commits, 9 cards.

Faz parte da cascata que leva o evolution-ecosystem para produção, parada em 2 de julho. A main do superprojeto aponta para SHAs que estão na main de cada submódulo, então cada repositório corta primeiro; o superprojeto bumpa os ponteiros por último.

Merge testado com git merge-tree antes de abrir: sem conflitos. A main está 2 commits à frente da develop; o merge preserva os dois lados.

Este repositório não depende dos demais dentro da cascata — pode ser mergeado em qualquer ordem em relação aos irmãos.

As migrations do conjunto foram ensaiadas contra uma cópia restaurada do banco de produção (Postgres 18.6, imagens por digest): cinco passos do migrate-job, todos exit 0, dado intacto, idempotência confirmada.

Runbook: https://claude.ai/code/artifact/3a9608d8-4126-4c9c-9934-8e0e9074fbff

Matheus Pastorini and others added 30 commits July 23, 2026 14:37
The Set Variable config UI offers numeric Increase/Decrease (preview shows +40),
but the runtime never read `operation` — SetVariableNodeInput.nodeData didn't even
declare it — so every op was a plain SET and "increase lead_score by 40" never
accumulated (silent-success family, EVO-1740/EVO-1757).

- Declare operation/value/category on nodeData (removes the `as any` casts).
- Add loadSessionVariables() (mirrors conditional.node.ts / EVO-1913) to read the
  current value.
- For increase/decrease: base = Number(prior) (unset/non-numeric prior -> 0),
  delta = Number(value); write base +/- delta as a number (so downstream numeric
  comparisons keep working). A non-numeric amount throws -> visible failure
  (success:false) instead of a silent no-op (AC #3). Plain SET and all other ops
  are unchanged. Scope: increase/decrease only.
- New set-variable.node.spec.ts (jest): increase from numeric/unset/non-numeric
  prior, decrease, plain set unchanged + doesn't read session, default set,
  non-numeric amount fails visibly. 7 tests pass; tsc clean.
…ric error (EVO-2203)

The pipeline nodes already turn a CRM failure into a visible node error via
createErrorResult(error), but a 422 threw BadRequestException(body) whose
message is generic — so an archived-pipeline refusal reached the journey run
as "Bad Request Exception". The 422 handler now lifts the CRM envelope's
error.message to the exception message, so the run shows the reason
("Pipeline is archived and cannot receive conversations") while getResponse()
keeps error.code for callers that branch on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…riting a wrong number

Code review follow-up on the increase/decrease fix. The arithmetic worked, but
every way it could NOT be honored still wrote a wrong value and reported
success — the same silent-success class the card set out to close (EVO-1740).

- Failed/missing session read no longer degrades to {}. Rebasing to 0 on an
  unreadable prior silently turned lead_score 500 into 40 with success:true.
  The read moves to BaseNode.readSessionVariables() (it was duplicated verbatim
  here and in conditional.node.ts) and throws; conditional keeps its local
  degrade-to-{} policy, set-variable lets it propagate.
- Empty/null amount no longer counts as 0. Number('') and Number(null) are 0,
  so an empty Amount was a silent "increase by 0" — while the panel renders 1
  as the placeholder in that state.
- A {{variable}} amount is resolved against the session before parsing. The
  panel's Amount field has a variable picker and the executor passes nodeData
  raw, so {{bonus}} arrived literal and aborted the whole journey.
- A non-numeric CURRENT value now fails instead of being clobbered to the delta
  (AC#3); genuinely unset/null/'' still starts at 0.
- The array input shape honors operation too — it kept degrading increase to a
  plain SET, the exact bug being fixed, on the other half of the contract.
- The error path reports a duration instead of Date.now() (an epoch timestamp
  was flowing into logNodeExecution/trackNodeExecution as the node's duration).

Tests: 17 in set-variable.node.spec.ts (was 7), covering accumulation across
runs, {{var}} amounts, the array shape, and each visible-failure case.
tsc clean; conditional/base specs unaffected (47 pass).
… actually use (EVO-2203)

Code review of #111. The 422 message lift landed in requestGeneric, whose only
consumer is contacts-client. The three pipeline nodes call addToPipeline /
moveToPipelineStage / createPipelineTask, which go through executeRequest — its
422 branch was untouched, so the journey run kept showing the raw JSON envelope
("CRM Validation error: {\"success\":false,\"error\":{...}}"). describeCrm422 now
folds code and reason into that string. The "CRM Validation error" prefix stays:
executeRequest matches on it to not retry a refusal.

Spreading the envelope also overwrote a top-level `message` with the placeholder,
so a 422 from render_record_invalid's fallback lost its reason ("Email is
invalid" became "CRM rejected the request"). crmRejectionReason now prefers
error.message, then a top-level message, then the raw body.

Tests: the refusal is covered where the nodes read it (client level, both
endpoints, no retry) and end to end on assign-to-pipeline with a real client —
the node specs mock the CRM client away, which is why the gap went unseen.
…tive

Comments only, no behavior change. Each one kept the non-obvious decision and
dropped the before/after story, which belongs in the PR, not the source.
…riable-increment

fix(EVO-1840): Set Variable node honors Increase/Decrease at runtime
feat(evo-flow): surface the CRM rejection reason on a 422, not a generic error (EVO-2203)
…ey/campaign node

The EVO-1716 cutover removed the inbox-nested GET route
(/api/v1/inboxes/:inbox_id/message_templates) and moved template listing to the flat
/api/v1/message_templates?inbox_id=... endpoint. Frontend, controller, policy and the
CRM's message_templates_service_token_spec were updated, but CrmClientService in
evo-flow still called the removed nested route -> 404 -> resolveTemplate returned null
-> a send-message node in messageMode: 'template' silently skipped the send. So every
journey/campaign template message stopped being delivered.

Fix: getInboxMessageTemplates now hits the flat endpoint with inbox_id as a query
param. The existing resolveTemplate parsing (Array.isArray(raw?.data)) already matches
the flat endpoint's success envelope, so nothing else changes.

QA (jest + tsc): 3 suites / 40 tests green incl. a new crm-client.service.spec contract
test that pins the flat URL (same pattern as the EVO-1272 moveToPipelineStage guard,
which a mocked-client node spec cannot catch). Red counter-check: reverting to the
nested URL fails that test (received the removed /inboxes/:id/... path). typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r (CRM-60)

SegmentClickHouseQueryBuilderService interpolated segment definition
values (customAttributeName, value, prop.path, labelId, templateId)
directly into ClickHouse string literals, with no escaping. A value
containing a single quote breaks out of the literal inside the
INSERT INTO ... SELECT that computes segment membership.

Route every user-controlled string through the existing (previously
unused) SegmentQueryUtils.sanitizeStringValue helper before
interpolation, and coerce numeric comparison operands through
Number()/Number.isFinite instead of splicing raw text outside quotes.
… (CRM-60)

The CustomAttribute node and the legacy UserProperty
customAttributes[.<attr>] path each built their own ClickHouse SQL
for the same delta-event read, and had already drifted: only the
CustomAttribute branch accepted the legacy custom_attribute_changed
event name and special-cased NotEquals/NotContains to include
contacts with no event for the attribute (a contact who never set it
trivially satisfies "not equal to X").

Extract both into a single buildCustomAttributeSubQuery, called by
both entry points. While unifying, extend the same "include every
contact, then flip on a positive match" handling to NotExists: it
previously fell through to the generic per-event condition, so a
contact who never triggered the attribute's change event got no row
in the state table and was silently excluded from a "does not have
this attribute" segment.
…s (CRM-60)

Review follow-up on the escaping pass: timesOperator/times from the
Performed node, the node id embedded in state_id, prototype-chain hits
on the message event map, and LIKE wildcards in Contains values.
…60-consolidate-custom-attribute-query-builder

fix(segments): consolidate custom attribute query builder, harden SQL
…-template-flat-endpoint

fix(CRM-209): call the flat message_templates endpoint from the journey/campaign node
…r e no cache de deletados (CRM-215)

O CRM emite contact.label.added/removed (labelId em traits) e contact.deleted;
o builder filtrava label_added/label_removed lendo properties e contact_deleted,
então segmento por etiqueta computava 0 membros e contato excluído seguia contando.

- Fonte única dos nomes (canônico + legado) e do subselect de deletados em
  queries/contact-event-names.ts, reusada pelo builder, pelo cache e pelo regex
  de reescrita da execução (que dependia do texto literal do CASE).
- Remove queries/contact-exclusion-queries.ts (sem chamador).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYhKfFRYqqdNnFTL7FSVpS
…ados estar quente (CRM-215)

O recompute é incremental e o cache de deletados tem TTL de 5 min: se a única
janela que continha o contact.deleted rodava com o cache vazio, o CASE virava
WHEN 1=0 e o contato seguia no segmento até um recálculo completo.

- Cache vazio mantém o subselect real (otimização nunca substitui correção).
- A API de eventos sinaliza a ingestão de contact.deleted via EventEmitter (sem
  dependência de módulo Nest; só constantes compartilhadas) e o cache invalida o
  snapshot e ignora o cache por 15 s — a ingestão no ClickHouse é assíncrona e um
  fetch imediato re-cachearia um conjunto ainda sem a exclusão.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYhKfFRYqqdNnFTL7FSVpS
… CASE (CRM-215)

Achado de review na propria CRM-215. applyDeletedContactsOptimization casava
`THEN '<qualquer>'` mas reescrevia sempre `THEN 'false'`. A maioria dos nos
(LastPerformed, Performed sem contagem, WhatsApp/Web/SMS, UserProperty
nao-argMax, e qualquer operador Exists) marca o contato deletado com o
sentinela VAZIO, e a associacao e `argMaxMerge(last_value) != ''` -- reescrever
para um literal nao-vazio devolvia o contato deletado ao segmento, justamente o
AC que o card veio fechar.

Regressao nova: o regex da develop (`[^)]*`) nao atravessava o `)` de
`argMax(occurred_at, occurred_at)` e nunca casava, entao a reescrita era codigo
morto. A validacao manual do card so exercitou nos Label, a unica familia com
`validationInfo: Equals 'true'` e ramo ja `'false'`, portanto imune.

- captura o literal do ramo e o reemite; replacer em funcao para que `$` dentro
  de um id nao seja lido como referencia de grupo
- routing-config: adiciona `contact.deleted` canonico, que caia no fallback
  SYSTEM/low em vez de LIFECYCLE (mesmo defeito ja corrigido para o dotted de
  custom attribute)
- remove o ramo morto de size === 0 no optimizeQueryWithDeletedContactsCache
- testes: sentinela vazio preservado nos tres tipos de no afetados, `$` no id,
  e classificacao das duas grafias do evento de exclusao
…M-215)

Achado 5 do review. A migracao titulo→id do PR #318 so roda quando alguem abre
e salva o segmento no editor, e nao ha backfill: toda definicao gravada pelo
editor antigo guarda o TITULO da etiqueta, entao o AC1 valia so para segmento
novo ou reeditado.

O evento ja carrega os dois lados -- o handle_label_change do CRM monta
`traits = { labelName:, labelId:, source: }` -- entao aceitar as duas grafias na
camada de query faz a definicao antiga voltar a casar sozinha, sem migracao e
sem depender de reabrir o segmento. Mesma postura de "aceitar ambos" ja adotada
para os nomes de evento.

Backfill de verdade sairia caro e frágil: as definicoes moram no Postgres do
evo-flow, mas o mapa titulo→id mora no CRM -- a migration teria que chamar a API
do CRM.
… /g (CRM-215)

Achados 9 e 10 do review, ambos no arquivo novo da propria PR.

- sqlStringList dobrava aspas de ninguem: hoje so recebe constantes, mas e
  exportado e uma string com aspas quebraria o literal.
- o regex do ramo CASE era uma constante exportada com flag /g, e .test() nela
  carrega lastIndex entre chamadas. Virou funcao que devolve instancia nova.
…al-contact-event-names

fix(segments): casa os nomes canônicos de evento de contato e fecha a exclusão de deletados (CRM-215)
…(CRM-256)

WebhookTrigger casava qualquer eventName comecando por `webhook.`. O pipeline de
entregabilidade de e-mail grava cada callback de provedor em contact_events como
`webhook.<platform>` e a materialized view events_to_journey_triggers_mv publica
tudo no mesmo barramento, entao abertura/clique/bounce iniciava toda jornada com
gatilho Webhook — com contact_id vazio, porque a linha de analytics nao resolve
contato.

- Match exato em `webhook.journey_trigger`, o nome que
  POST /api/v1/journeys/trigger/:journeyId emite; `eventName` na config do
  trigger sobrepoe o padrao.
- Quando o evento traz `journeyId` nas properties, ele precisa ser o desta
  jornada — senao o webhook de uma jornada dispararia todas as outras.

A MV segue sem WHERE de proposito: os outros sete tipos de gatilho leem do mesmo
topico.
…-271)

analyzeEventForJourneyTriggers seguia adiante com contactId vazio: consultava
sessoes em espera pela chave '', casava gatilhos e chamava triggerJourneyExecution,
que iniciava workflow Temporal com contactId '' e workflowId degenerado
(journey-<id>-contact--<ts>). A guarda de reentrada e o claim de idempotencia
tambem passavam a ser chaveados por contato vazio, entao todos os eventos sem
contato compartilhavam a mesma chave.

O barramento journey-triggers recebe toda linha de contact_events, e nem todo
produtor resolve contato — as linhas de entregabilidade de e-mail gravam
contact_id vazio.

Descarte agora acontece antes de qualquer trabalho contact-scoped, com WARN
nomeando evento e messageId em vez de sumir em silencio.
…a de config (CRM-256)

Retorno de code review sobre o commit anterior.

O ramo que comparava properties.journeyId com journey.id saiu. Ele nunca
executava — nenhum produtor publica webhook.journey_trigger no barramento, ja
que o endpoint manual chama startJourney direto — e quebrava um contrato que os
outros sete handlers respeitam: ao avaliar condicoes de espera o processador
chama matches() com um journey vazio, entao qualquer decisao baseada em
journey.id tornava a espera insatisfazivel. Com isso somem tambem o JSON.parse
que falhava aberto (payload malformado passava a casar toda jornada) e o log
de erro em nivel debug.

A resolucao do nome do evento passa a seguir a mesma ordem do EventTrigger
(metadata, no, conditions), trata nome em branco como nao configurado e nao
estoura com trigger nulo. Antes, um no configurado via conditions.eventName era
ignorado em silencio — a mesma classe de bug, invertida.

Os testes cobrem as duas fontes novas de config, o call site de condicao de
espera e as assercoes de reason/metadata, que antes nao eram verificadas.
…spatch (CRM-271)

Retorno de code review sobre o commit anterior.

O descarte era logado em warn. Evento sem contato nao e anomalia: e a maior
parte deste barramento — todo callback de entrega de e-mail e todo clique
anonimo chegam assim. Em warn, o alerta de operacao dispararia continuamente.
Passa a debug por evento, com o total corrido em info a cada mil descartes, para
o volume continuar visivel sem virar ruido de alerta.

O comentario anterior justificava o guard pelo dedup, que na verdade e chaveado
por (jornada, contato, messageId) e portanto nunca colide entre eventos sem
contato. O custo real e outro: getSessionsByContact carrega todas as sessoes em
cache antes de filtrar, e uma sessao aberta sob contato vazio e devolvida a
qualquer outro evento sem contato.

O guard tambem passa a cobrir evento sem nome — nenhum dos oito handlers casa
sem nome — e triggerJourneyExecution ganha a mesma checagem, em error, ja que
chegar la sem contato significa que a barreira de entrada foi contornada.

Os testes passam a exercitar processMessage, que e por onde o consumidor entra,
e a construcao do processador virou fabrica compartilhada, sem a espera por
setImmediate que cada bloco repetia.
…o (CRM-241)

Toda condição whereProperties de nó Performed / LastPerformed era montada como
JSONExtractString(properties, '<path>'), mas os eventos de contato que o CRM emite
chegam ao ClickHouse como `identify`: o payload inteiro vai em `traits` e a coluna
`properties` fica `{}` (EvoFlow::PayloadBuilder.build_identify não monta a chave
`properties`). O filtro nunca casava linha nenhuma, e falhava em SILÊNCIO nos dois
sentidos — o SQL era válido, rodava sem erro e simplesmente lia a coluna errada:

  - Equals / Contains / Exists  -> '' = 'VIP' sempre falso    -> segmento vazio
  - NotEquals / NotContains     -> '' != 'VIP' sempre verdade -> o filtro NÃO
    filtra e todo mundo entra. Numa campanha, é público ERRADO, não vazio.

A extração passa a escolher a COLUNA e extrair uma vez:

  JSONExtractString(if(JSONHas(properties, 'p'), properties, traits), 'p')

`JSONHas`, e não `!= ''`, porque uma chave presente com valor vazio é resposta
legítima do produtor: cair para `traits` nesse caso trocaria um vazio deliberado
pelo valor de outra fonte e quebraria NotExists. Prefere `properties` e só cai para
`traits` quando a chave não existe, então evento `track` — que grava `properties` e
deixa `traits` em `{}` — produz SQL equivalente ao de antes. Escolher a coluna (em
vez de extrair das duas e comparar) mantém em duas operações JSON por linha.

`template_id` do nó WhatsApp/Web/SMS ficou intacto de propósito: não é
whereProperties e lê `properties` legitimamente.

Performed e LastPerformed carregavam switches DUPLICADOS para montar essa condição,
e eles estavam fora de sincronia: o do LastPerformed não listava `GreaterThan`,
`GreaterThanOrEqual`, `LessThan`, `LessThanOrEqual` nem `NotExists`, que caíam no
`default` e viravam IGUALDADE — também em silêncio, porque o SQL seguia válido
("score > 10" selecionava score == 10). Em vez de completar a lista da cópia, o
filtro virou um método só (`buildEventPropertyCondition`) usado pelos dois nós: a
causa era a duplicação. Operador desconhecido continua caindo em igualdade, mas
agora loga warning em vez de degradar calado.

Testes:

- Spec do builder (35 exemplos) fixando o SQL por operador, o escaping de path e
  valor, a não-regressão do template_id, e um bloco de PARIDADE que exige condição
  idêntica entre Performed e LastPerformed nos 10 operadores — é o teste que quebra
  se a cópia for recriada.
- Teste de integração (test/segment-where-properties.e2e-spec.ts, 8 exemplos) que
  EXECUTA a condição gerada contra um ClickHouse real e assere os CONTATOS
  retornados, que é o critério de aceite do card. O spec unitário não pega este
  bug: a expressão antiga é SQL válido, o que falhava era o casamento. Por isso um
  dos exemplos roda a expressão ANTIGA contra as mesmas linhas e prova que ela erra
  nas duas direções — o teste não é tautológico.

Provas negativas: revertendo a leitura de traits, 18 dos 20 exemplos originais
falham e 6 dos 8 do e2e; reintroduzindo o switch reduzido do LastPerformed, falham
exatamente os 9 que cobrem os operadores numéricos e a paridade.

Notas do teste de integração:

- FALHA se o ClickHouse não estiver acessível, em vez de se auto-pular. Um skip
  condicional deixaria a suíte VERDE sem ter exercitado nada — o mesmo modo de
  falhar silencioso que este commit corrige. (E `it.skip` por flag nem funcionaria:
  o Jest avalia isso na coleta, antes do beforeAll.)
- Usa uma tabela ESPELHO (`CREATE TABLE ... AS contact_events`), criada e destruída
  pelo teste. `clickhouse.service.ts` cria a `events_to_journey_triggers_mv`
  escrevendo num engine Kafka SEMPRE, inclusive onde o broker é RabbitMQ
  (`BROKER_TYPE=rabbitmq`, o compose community); sem Kafka atendendo, o INSERT na
  tabela real fica preso até o timeout. A espelho mantém o teste determinístico nos
  dois ambientes, com schema idêntico e sem tocar na tabela de produção.
- `async_insert: 0` fixado no cliente: o ClickHouse do community traz
  `async_insert=1` no users.xml e o do ecosystem fica no default 0; com
  `wait_for_async_insert=1` (ligado nos dois) o cliente ficaria preso ao flush do
  buffer.

Validado nos dois ambientes, 8/8 em cada:
  community  :18123  ClickHouse 26.7  broker rabbitmq
  ecosystem  :18124  ClickHouse 25.8  broker kafka

Além do e2e, a expressão foi conferida direto na `contact_events` REAL do ecosystem
(sem espelho): properties={} / traits={"labelName":"VIP"}, expressão antiga devolve
vazio e a corrigida devolve VIP — o bug e o fix reproduzidos na tabela de produção,
com Kafka e MVs ativos.

Suíte completa: 1099 exemplos passando. As 4 suites que falham são pré-existentes
(campaigns.controller + 3 nodes temporal/evoai) e falham igual sem este commit.
tsc --noEmit limpo.
…ça os processadores real-time (CRM-241)

Achados do code review do PR #118.

O filtro de propriedade de evento vira um só, em `SegmentQueryUtils`
(`extractEventProperty` + `buildEventPropertyCondition`). O builder do ClickHouse
delega: os 35 exemplos do spec seguem passando sem qualquer ajuste, o que prova
que o SQL emitido é idêntico.

`atomic-processor.service.ts` (2 sítios) e `batch-processor.service.ts` carregavam
mais duas cópias do MESMO filtro, lendo só `properties` — ou seja, o bug que o
CRM-241 corrige, numa quarta e numa quinta superfície. São alcançáveis com
`SEGMENT_COMPUTATION_TYPE=real-time`. Além da coluna errada, interpolavam
`path`/`value`/`event`/`labelId` crus, sem escaping, e a cópia do batch lia
`prop.key`/`prop.value` — shape que o front nunca emite, então o filtro resolvia
para a string literal "undefined". As três passam a usar o builder compartilhado.

O e2e passa a ser opt-in (`SEGMENT_E2E=1`), como o `tenant-isolation.e2e-spec.ts`
da mesma pasta, para não somar uma suite falhando ao `npm run test:e2e`. A conexão
vem das envs que o projeto já usa (`CLICKHOUSE_HOST`/`PORT`/`DATABASE`) em vez de
`CLICKHOUSE_URL`/`CLICKHOUSE_DB` com default 18123, que não bate com o compose do
próprio repo (8123). Ligado, ele continua FALHANDO — não pulando — se o servidor
não responder. O `afterAll` disparava um segundo erro sem contexto quando o
`beforeAll` falhava (o client já estava atribuído; `createClient` é lazy): agora só
limpa o que criou, e varre tabelas espelho deixadas por uma run interrompida.

Comentários novos em inglês e enxutos (CLAUDE.md), e o comentário que dizia que o
path é interpolado três vezes estava errado: são duas (JSONHas + JSONExtractString).

Testes: spec novo com 7 exemplos cobrindo os três sítios do módulo de processing
(fallback para traits, escaping de aspas em path/valor/evento, e a não-regressão do
shape errado do batch). Suíte completa: 1079 passando (era 1072); as 4 suites que
falham são as mesmas pré-existentes (campaigns.controller + 3 nodes temporal/evoai).
e2e 8/8 contra ClickHouse real, `tsc --noEmit` limpo.
…roperties-traits

fix(segments): whereProperties precisa ler traits em evento de contato (CRM-241)
…-trigger-discriminates-event

fix(journeys): gatilho de webhook casa o evento exato, nao o prefixo (CRM-256)
…ig do no (CRM-256)

Retorno de code review sobre o #116, ja mesclado.

O override de nome de evento saiu. Nenhuma UI de nó Webhook escreve esse
campo — a WebhookConfiguration so tem URL, metodo e headers —, mas o editor
copia data.eventName para conditions e metadata de TODO nó de gatilho
(journeyFlowTriggers.ts) e o painel nao limpa o campo ao trocar o tipo
(JourneyTriggerPanel.tsx:118). Um nó configurado como Evento e depois trocado
para Webhook carregava o nome antigo, e o handler passava a casar esse nome em
vez de webhook.journey_trigger: a jornada disparava no evento errado, agora com
contato real, e o caminho legitimo deixava de casar.

Com o override foram junto o .trim() sobre valor vindo de jsonb — que estoura
com eventName nao-string e aborta a analise da mensagem para todas as jornadas,
nao so a mal configurada — e o wrapper decide(), que so logava.

O doc passa a registrar tambem o nó Aguardar evento -> Webhook: mesmo handler,
roteado por eventType e avaliado com journey vazio, config sem nome de evento,
logo satisfeito so por webhook.journey_trigger — hoje, por nada. Sem
enableFallback essa espera nao tem timeout.

Testes: as assercoes das fontes de config dao lugar a duas que provam o buraco
fechado — um nó com o nome herdado nos tres lugares nao casa esse nome e
continua casando webhook.journey_trigger.
gomessguii and others added 10 commits August 23, 2026 22:28
…-trigger-fixed-target

fix(journeys): gatilho de webhook casa so o evento fixo, sem ler config do no (CRM-256)
…(CRM-271)

Retorno de code review sobre o PR #117.

O descarte por evento era logado em debug, e CustomLoggerService.debug retorna
antes do console e antes do winston — a linha nao saia em lugar nenhum. O
criterio de aceite pede descarte visivel, e o que restava era o total corrido a
cada mil, que nao nomeia evento algum: os 999 primeiros ficavam silenciosos. O
teste que cobria isso afirmava contra o mock de logger.debug, entao seguia verde
com a producao muda. O descarte passa a log/info.

O volume em info nao muda: processMessage ja emite duas linhas por mensagem (uma
delas com o evento inteiro serializado) e analyzeEventForJourneyTriggers uma
terceira. Essa terceira anunciava analise para evento que seria descartado — a
guarda subiu para antes dela, entao o evento descartado gasta uma linha a menos,
nao uma a mais.

Quando o que falta e o nome, a mensagem virava "Skipping event undefined"; passa
a <unnamed>, e o contexto carrega messageId e anonymousId, que para as linhas de
entregabilidade e a unica alca de volta ao evento de origem.

O total corrido so imprimia em multiplos exatos do intervalo, entao ate 999
descartes se perdiam a cada restart; onModuleDestroy passa a liberar o saldo.
…mpty-contact-id

fix(journeys): evento sem contato nao inicia execucao de jornada (CRM-271)
O metodo montava um evento webhook.received completo e nunca o publicava, e sanitizeHeaders so era usado por ele. Quem lia esse trecho procurando o caminho do webhook concluia que a ingestao existia.
A remocao do metodo morto tira a pista falsa e nao deixa nada no lugar. O README passa a dizer qual e o unico gatilho de webhook do modulo e que o POST /webhooks/* e outro pipeline.
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
… (CRM-257)

O endpoint nao publica o webhook.journey_trigger em lugar nenhum: monta o
evento e passa para startJourney como payload do workflow, sem tocar o
barramento journey-triggers. Dizer "publishes" contradiz o
docs/journey-manual-trigger.md, que ja documenta o caminho certo — a secao
agora aponta para ele em vez de reescreve-lo.

O trecho do POST /webhooks/* tambem afirmava isolamento que nao existe: a MV
events_to_journey_triggers_mv encaminha toda linha de contact_events para o
barramento, e o que segura os callbacks de provider sao os dois guards
(contact_id vazio e match por nome exato), nao o pipeline ser outro.
…e-dead-webhook-trigger

chore(journeys): remove processWebhookTrigger sem callers (CRM-257)
The note itself documents runtime behaviour and stays; only the link to
the internal issue tracker is removed.
…link

docs(broker): drop the tracker link from the redelivery note

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @gomessguii, your pull request is larger than the review limit of 150,000 diff characters

@gomessguii
gomessguii merged commit 6a0d123 into main Sep 2, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants