feat(error-logs): record the channel-side contact id and stack trace on provider failures - #1139
Merged
Merged
Conversation
hoangle398
force-pushed
the
fix/flow-error-logs
branch
2 times, most recently
from
September 10, 2026 16:10
a914e13 to
138212e
Compare
…ures Adds `ErrorLog.sourceId` — the contact's channel-side identity (Messenger PSID, IGSID, WhatsApp `wa_id`) — so a provider failure is attributable even when `contactId` is null, which is exactly the case that matters most: the contact row does not exist yet (a creation-path `getProfile` failure, a Lead Ads lead). - new nullable `sourceId` column plus migration; no FK, since `ContactInbox.sourceId` is unique only per `inboxId` and the row must outlive the contact inbox entirely - threads the id through `eventContextSchema` and the error-log event payload, and onto the `logProviderError` call sites that have a contact in scope - withholds it from the `analytics`-scoped public API: the same identity is public only under the `contacts` scope, so the response is an explicit allow-list and the list's keyword search skips the column - reformats the Graph error code prefix to `(#10 - …)`, matching Meta's own shape so re-formatting is idempotent
`ErrorLog` said what failed and who it concerned, but not where in our code the failure surfaced — with ~41 call sites, and five Google Sheets operations sharing one helper, a row was often ambiguous. The stack is stored in a new developer-only `ErrorLog.stackTrace` column and withheld from every read surface: `ErrorLog` is workspace-facing (the builder table plus the workspace-token API) and a stack leaks absolute server paths and our internal call chain. `WITHHELD_ERROR_LOG_COLUMNS` declares the policy once and the DB projection, response schemas and UI resource type all derive from it, so a future developer-only column is withheld from all of them by being named there. Sorting is now allow-listed (`SORTABLE_COLUMNS`) rather than gated on "is a real column": ordering by a column the caller cannot read is a lexicographic oracle over it, so the allow-list keeps the next withheld column safe on the day it lands. Callers now hand the service the raw throw instead of a normalized copy — `normalizeError` returns a plain object, so `error instanceof Error` failed and every one of those rows stored a NULL stack.
hoangle398
force-pushed
the
fix/flow-error-logs
branch
from
September 12, 2026 08:10
f754798 to
0d83ec3
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.
sourceId— whoAdds
ErrorLog.sourceId— the contact's channel-side identity (Messenger PSID, IGSID, WhatsAppwa_id) — so a provider failure is attributable even whencontactIdis null, which is exactly the case that matters most: the contact row does not exist yet (a creation-pathgetProfilefailure, a Lead Ads lead).sourceIdcolumn plus migration; no FK, sinceContactInbox.sourceIdis unique only perinboxIdand the row must outlive the contact inbox entirelyeventContextSchemaand the error-log event payload, and onto thelogProviderErrorcall sites that have a contact in scopeanalytics-scoped public API: the same identity is public only under thecontactsscope, so the response is an explicit allow-list and the list's keyword search skips the column(#10 - …), matching Meta's own shape so re-formatting is idempotentstackTrace— whereErrorLogsaid what failed and who it concerned, but not where. With ~41 call sites — and five Google Sheets operations sharing one helper — a row was often ambiguous about which one produced it.ErrorLog.stackTracecolumn plus migration, capped and stripped of the message prefix byresolveStackFrames(@chatbotx.io/utils/error-log)ErrorLogis workspace-facing (the builder table plus the workspace-token API) and a stack leaks absolute server paths and our internal call chain.WITHHELD_ERROR_LOG_COLUMNSdeclares the policy once; the DB projection, both response schemas and the UI resource type all derive from it, so the next developer-only column is withheld from all of them by being named thereSORTABLE_COLUMNS) rather than gated on "is a real column on the model": ordering by a column the caller cannot read is a lexicographic oracle over it, so the allow-list keeps the next withheld column safe on the day it landsnormalizeErrorreturns a plain object, soerror instanceof Errorfailed and every one of those rows stored a NULL stack.resolveMessagegained a readable fallback so a message-less object throw no longer writes[object Object]into the workspace-facingdetailParsedErrorbefore it reaches the Redis stream, so it captures frames at the emit site and forwards them explicitly — otherwise the densest path in the system could never derive a stacklogStepProviderErrorhelperVerification
pnpm --filter worker test(2227),pnpm --filter builder test(3466),pnpm --filter @chatbotx.io/business test(2308),check-typeson builder + business, and the@chatbotx.io/databasedrift check — all clean.The migration is generated but not applied — it needs the usual approval before
db:migrateruns anywhere.