Add sentry error tracking - #170
Merged
Merged
Conversation
When the app hit an error, Sentry attached a copy of every variable in scope in every frame of the traceback, and a database outage therefore sent the database password. `psycopg.connect()` is called with keyword arguments, which psycopg assembles into one string containing `password=...` held in a local variable named `conninfo`. The event scrubber could not withhold it: it compares variable names, never their contents, and the name here is `conninfo`. Reproduced with the app's own `get_db_connection`, the password appearing at `$.exception.values[0].stacktrace.frames[2].vars.conninfo` — on the path the service loop takes, where the failure is caught and logged with the exception attached. Variable values are therefore no longer sent at all. One option covers every form the credentials take, not just this one: the connection string built for yoyo, both Azure connection strings, and Azure's `account_key`, whose name is not in Sentry's own list of names to withhold. Every frame keeps its file, line, function and source line; only the values are lost. Sentry also records the query string of every outgoing request with its values intact. An Azure storage connection string which uses a shared access signature puts that signature in the query string of each request, so `before_breadcrumb` and `before_send_transaction` withhold it. The method, the URL without its query string and the response status are kept. The regression test drives the real library rather than a stand-in, so it keeps testing the real behaviour if psycopg changes how it holds the connection string, and it asserts that psycopg was actually reached so that it cannot pass by exercising the wrong path.
Errors about individual SuiteCRM records name the record they are about, and Sentry groups events made from log messages on their message text, so each bad record arrived as its own issue and its own email. Fifty inconsistent records meant fifty alerts about one problem, which is the situation described on #90. A call site now marks such an error with `extra={"bds_alert_group": "..."}`, and a `before_send` hook turns that into a fingerprint so occurrences accumulate on one issue. It is recorded as a tag as well, so an alerting rule can be written against a class of alert rather than against message text. `extra` is a standard `logging` keyword, so no call site refers to Sentry: the adapter in config/sentry.py remains the only module which does. The messages themselves are unchanged, and so is the log output — the formatter renders only `%(message)s`, so moving the record identifiers out of the message would have removed them from the logs. Applied to all four per-record errors in iati_registry_suitecrm.py rather than only the reporting-org case raised on #90, since all four interpolate a record identifier and so fragment in the same way.
simon-20
previously approved these changes
Sep 2, 2026
simon-20
left a comment
Contributor
There was a problem hiding this comment.
This is great @arobson-ods, all the right calls on the scrubbing I think. It'll be a huge improvement over what we have, and if the missing info really turns out to be problematic we can revisit.
The only very minor thing:
- I think we should remove the
ruffconfiguration, to avoid confusion. If it's theruffVS Code extension that this is designed to placate, then it is possible to turn that off per workspace.
Contributor
Author
|
Many thanks @simonk. Of course you are right. I overreacted to trying to fix what looked like an isort problem and finding it was ruff. |
simon-20
previously approved these changes
Sep 3, 2026
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.
Overview
Errors are now reported to [Sentry](https://sentry.io), covering both halves of #90: unexpected
exceptions and
RuntimeErrors are streamed, and theERRORcases raised in the SuiteCRM records whose reporting org does not exist are grouped so that repeat occurrences produce one issue rather than one per record.What changed
Error reporting —
src/config/sentry.py(new).initialise_sentryis called fromsrc/iati_bulk_data_service.pybefore anything else in startup which can fail, so failures during the rest of startup are reported. Every event carries the app version as its release, the environment, and abds.operationtag naming which ofchecker/zipper/registry-changes-processorit came from, since the three run as separate containers reporting to one project.Caught exceptions now reach Sentry:
checker.py,registry_changes_processor.py.The service loops deliberately swallow exceptions and retry. They logged the exception as an interpolated string and, in the checker, the traceback as a second log line. This means Sentry would have received two message-only events with no exception type and no stack frames, grouped on text which embedded the varying error message. They now use
logger.exception, which yields one typed event with real frames.Per-record SuiteCRM errors group by class of alert:
iati_registry_suitecrm.py,src/config/sentry.py.Sentry groups events made from log messages on their message text, and these messages name the record they are about, so fifty inconsistent records meant fifty issues and fifty emails. These errors are marked with
extra={"bds_alert_group": "..."}and abefore_sendhook turns that into a fingerprint plus abds.alert_grouptag.Credentials are withheld from events —
src/config/sentry.py. See Architectural decisions.Configuration — three variables (
SENTRY_DSN,SENTRY_ENVIRONMENT,SENTRY_TRACES_SAMPLE_RATE), classified inconfig.py, added to.env-example, the ARM template, the deploy workflow and the manual-deploy examples.Tooling —
[tool.ruff] line-length = 119inpyproject.toml. Ruff is not project tooling, but the Ruff editor extension is widely installed and defaults to 88. This can cause churn in import statements which can be quite tricky to pin down.Architectural decisions and scope
No Sentry API is called from application code. The only module which imports
sentry_sdkissrc/config/sentry.py. Calls are expressed in standard-library terms -logger.exception(...)andextra={"bds_alert_group": ...}- and the adapter translates that into Sentry's model. This is whylogger.exceptionwas preferred oversentry_sdk.capture_exception(e): both produce identical events, and only one couples the app to a vendor.Stack-frame variables are not sent at all (
include_local_variables=False).This is the significant decision in the PR and it fixes a real leak found in review: Sentry attaches a copy of every variable in scope in every frame, and
psycopg.connect()assembles its keyword arguments into one string containingpassword=...held in a local namedconninfo. So a database connection failure would send the password to Sentry in a variable unknown to the event scrubber. Setting this one option closes every form the credentials take, not just that one: the connection string built for yoyo, both Azure connection strings, and Azure'saccount_key, whose name is not in Sentry's own denylist either.This is a trade-off with a significant cost: tracebacks keep every frame's file, line, function and source line, but lose the values of variables so a failure processing a dataset no longer shows the record. The log breadcrumbs attached to each event name the dataset being processed, which mitigates it.
Query strings of outgoing requests are withheld. Sentry records them with values intact. An Azure storage connection string using a
SharedAccessSignature=rather than anAccountKey=puts that signature in the query string of every blob request, and the environments vary, sobefore_breadcrumbandbefore_send_transactionfilter it either way. Method, base URL and status code are kept.Database connection metadata in exception text is accepted, deliberately. psycopg's message names the host, port, user and database. Nothing can scrub exception messages. Those four are marked
SECRETout of caution rather than because they are credentials, and suppressing them would make a database outage harder to diagnose.DB_PASSdoes not appear there. The README records this, along with the other things error reporting does not protect.Out of scope
warningtoerrorinside a loop retrying every 15s, so a lasting fault is roughly 5,700 events a day. Better handled with a Sentry per-key rate limit than by reverting the level.iati_registry_suitecrm.py:85passes1wherelibsuitecrmexpectsstr. Harmless at runtime; unrelated.DB_HOST/DB_PORT/DB_USER/DB_NAMEasLOGGABLE, so the policy matches thedecision above. Not done here because it would also add them to the startup log.
Testing
Beyond the unit and integration tests, the behaviour was verified against the real SDK.
$.exception.values[0].stacktrace.frames[2].vars.conninfo; with the fix it appears nowhere.SECRETvariable was checked with unique sentinel values. None reached the payload.[Filtered]while method, URL and status remain.Notes to reviewer
Things worth checking specifically:
requests-oauthlibsends the client id and secret asBasic base64(id:secret)c) felt inherently error-prone as a method. Possible enhancement: makeinclude_local_variablesconfigurable so at least we can get the full debugging info in the dev environment.iati_registry_suitecrm.py. The comment on Add Sentry error streaming #90 named one case; allfour per-record errors interpolate a record identifier and so fragment identically, so all four are marked.
registry_changes_processor.py's catch-all moved fromwarningtoerror. Previously an unexpected error in that loop would not have reached Sentry at all. It will now create issues. The two transient handlers (ServiceBusConnectionError,MessagingEntityNotFoundError) are deliberately left atwarning, being expected reconnection conditions rather than faults.[tool.ruff]inpyproject.tomlis config for a tool the project does not use and while it will be ignored ifruffisn't installed adding it is perhaps a bit questionable.Before this is merged
bds.alert_groupexists so a rule can target a class of alert rather than message text. Worth setting an interval, since a persistent fault re-reports every cycle.