Repository: getsentry/sentry-python

Issue:
Crash in pure_eval
This happened while we were experiencing a DB outage:
```
  File "/usr/local/lib/python3.8/dist-packages/asyncpg/connection.py", line 443, in fetch
    return await self._execute(query, args, 0, timeout)
  File "/usr/local/lib/python3.8/dist-packages/asyncpg/connection.py", line 1445, in _execute
    result, _ = await self.__execute(
  File "/server/athenian/api/db.py", line 191, in _asyncpg_execute
    result = await self._execute_original(query, args, limit, timeout, return_status)
  File "/usr/local/lib/python3.8/dist-packages/asyncpg/connection.py", line 1454, in __execute
    return await self._do_execute(query, executor, timeout)
  File "/usr/local/lib/python3.8/dist-packages/asyncpg/connection.py", line 1476, in _do_execute
    result = await executor(stmt, None)
  File "asyncpg/protocol/protocol.pyx", line 196, in bind_execute
    return await waiter
asyncpg.exceptions.ConnectionDoesNotExistError: connection was closed in the middle of operation

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/sentry_sdk/scope.py", line 353, in apply_to_event
    new_event = event_processor(event, hint)
  File "/usr/local/lib/python3.8/dist-packages/sentry_sdk/integrations/pure_eval.py", line 79, in add_executing_info
    pure_eval_frame(tb.tb_frame) or sentry_frame["vars"]
  File "/usr/local/lib/python3.8/dist-packages/sentry_sdk/integrations/pure_eval.py", line 128, in pure_eval_frame
    expressions.sort(key=closeness, reverse=True)
  File "/usr/local/lib/python3.8/dist-packages/sentry_sdk/integrations/pure_eval.py", line 113, in closeness
    nodes_before_stmt = [
  File "/usr/local/lib/python3.8/dist-packages/sentry_sdk/integrations/pure_eval.py", line 114, in <listcomp>
    node for node in nodes if node.first_token.startpos < stmt.last_token.endpos
AttributeError: 'Name' object has no attribute 'first_token'
```


Discussion:
cc @alexmojaki 
I'll defer to @alexmojaki. Also let's wrap all of this in `with capture_internal_exceptions` in a way where the SDK may fail to attach locals, but does not crash the app
@untitaker I'm pretty confused about when to use `capture_internal_exceptions`. Other integrations seem to use it sparingly. All the pure_eval logic is reached via `@add_global_event_processor` and thus this code in scope.py:

```python
        for event_processor in chain(global_event_processors, self._event_processors):
            new_event = event
            with capture_internal_exceptions():
                new_event = event_processor(event, hint)  # seen in traceback above
```

so it seems like `capture_internal_exceptions` should already wrap the entire thing. How did the error cause a complete crash?
On rereading teh stacktrace it perhaps didn't, you're completely right so ignore my comment.

Resolving patch:
diff --git a/sentry_sdk/integrations/pure_eval.py b/sentry_sdk/integrations/pure_eval.py
--- a/sentry_sdk/integrations/pure_eval.py
+++ b/sentry_sdk/integrations/pure_eval.py
@@ -104,23 +104,29 @@ def pure_eval_frame(frame):
     expressions = evaluator.interesting_expressions_grouped(scope)
 
     def closeness(expression):
-        # type: (Tuple[List[Any], Any]) -> int
+        # type: (Tuple[List[Any], Any]) -> Tuple[int, int]
         # Prioritise expressions with a node closer to the statement executed
         # without being after that statement
         # A higher return value is better - the expression will appear
         # earlier in the list of values and is less likely to be trimmed
         nodes, _value = expression
+
+        def start(n):
+            # type: (ast.expr) -> Tuple[int, int]
+            return (n.lineno, n.col_offset)
+
         nodes_before_stmt = [
-            node for node in nodes if node.first_token.startpos < stmt.last_token.endpos
+            node for node in nodes if start(node) < stmt.last_token.end
         ]
         if nodes_before_stmt:
             # The position of the last node before or in the statement
-            return max(node.first_token.startpos for node in nodes_before_stmt)
+            return max(start(node) for node in nodes_before_stmt)
         else:
             # The position of the first node after the statement
             # Negative means it's always lower priority than nodes that come before
             # Less negative means closer to the statement and higher priority
-            return -min(node.first_token.startpos for node in nodes)
+            lineno, col_offset = min(start(node) for node in nodes)
+            return (-lineno, -col_offset)
 
     # This adds the first_token and last_token attributes to nodes
     atok = source.asttokens()


Test patch: