fix(chatwoot-adapter): tell the operator why a relay failed - #66
Merged
Conversation
A relay that could not reach Chatwoot recorded its reason only in the host log, which the dashboard does not render. The health check reported bare counts, so "1 dead-lettered after 5 attempts" was the whole of what an operator could see — enough to know something was wrong, not enough to act. The health check now carries the last relay error alongside the counts, and only while something is failing, so a healthy plugin does not display a stale one. Also rejects a baseUrl carrying a path at config-read time. The client appends /api/v1/accounts/<id> to it, so a URL copied from the dashboard's address bar enabled cleanly and then 404'd every request, with the failure looking identical to a wrong token or an unreachable host.
rmyndharis
force-pushed
the
fix/chatwoot-adapter-relay-diagnostics
branch
from
August 5, 2026 14:03
79456a8 to
4812af4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
When an inbound message fails to reach Chatwoot, the adapter records the reason — the API status and response body, a refused private address, a certificate error — through
deps.log, which reaches the host's stdout and nowhere else. The dashboard renders a plugin'shealthCheckmessage, and that message carried only counts.So the entire diagnostic an operator gets is:
That is enough to know something is wrong and not enough to do anything about it. It also takes about three minutes to appear, because the count only moves once the retry budget is exhausted. A wrong API token, a
baseUrltypo, an unreachable self-hosted instance and a contact-identity collision all produce that same sentence.See #63, where an operator spent days on a first-time setup with no way to see what was being rejected.
The changes
The health check now carries the last relay error. A failed relay records its message, and
healthCheckappendslast error: …alongside the counts. It is recorded on the first failure rather than at dead-letter time, so the reason is visible while the message is still pending retry instead of three minutes later. It is appended only when something is actually failing — a healthy plugin does not display a stale error — and truncated to 300 characters.A
baseUrlcarrying a path is rejected when settings are saved. The client appends/api/v1/accounts/<id>to this value, so a URL copied out of the Chatwoot dashboard's address bar produces a request path like:That 404s on every request, while the plugin enables cleanly and reports healthy until the retries run out. Config validation already rejected a non-https or credentialed
baseUrlfor the same reason; it just never looked at the path. It now has to be the origin.Notes
onRelayErroris a required member ofInboundDeps, matching the existingonInboundLostandonBackfillExhaustedcallbacks. Making it optional would let a missed wiring pass silently, which is the failure mode this PR exists to remove.An enable-time connectivity preflight was considered and deliberately left out. A preflight that throws would block enabling the plugin whenever Chatwoot is briefly unreachable, and the last-error surface already covers every failure class rather than only the ones a preflight would catch. If it is wanted later, the safe shape is a non-blocking probe whose result is reported through
healthCheck.This does not close #63 on its own — the reporter's actual cause is still unknown, and there are several candidates. It does mean the next operator in that position can read the answer off the dashboard instead of filing an issue.
Verification
537/537 tests passing,
npx tsc --noEmitclean. Both behaviours are covered by new tests written against the failing state first: one asserts a path-bearingbaseUrlis rejected while bare origins (with port, with trailing slash) still pass, the other drives a real failing relay through themessage:receivedhook and asserts the reason reacheshealthCheck.