Skip to content

Commit

Permalink
Extract additional expression values with pure_eval (#762)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmojaki authored Jul 15, 2020
1 parent 5c34ead commit 2b8d96d
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 0 deletions.
4 changes: 4 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-executing.*]
ignore_missing_imports = True
[mypy-asttokens.*]
ignore_missing_imports = True
[mypy-pure_eval.*]
ignore_missing_imports = True
104 changes: 104 additions & 0 deletions sentry_sdk/integrations/pure_eval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from __future__ import absolute_import

import ast

from sentry_sdk import Hub
from sentry_sdk._types import MYPY
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.utils import walk_exception_chain, iter_stacks

if MYPY:
from typing import Optional, Dict, Any
from types import FrameType

from sentry_sdk._types import Event, Hint

try:
import executing
except ImportError:
raise DidNotEnable("executing is not installed")

try:
import pure_eval
except ImportError:
raise DidNotEnable("pure_eval is not installed")

try:
# Used implicitly, just testing it's available
import asttokens # noqa
except ImportError:
raise DidNotEnable("asttokens is not installed")


class PureEvalIntegration(Integration):
identifier = "pure_eval"

@staticmethod
def setup_once():
# type: () -> None

@add_global_event_processor
def add_executing_info(event, hint):
# type: (Event, Optional[Hint]) -> Optional[Event]
if Hub.current.get_integration(PureEvalIntegration) is None:
return event

if hint is None:
return event

exc_info = hint.get("exc_info", None)

if exc_info is None:
return event

exception = event.get("exception", None)

if exception is None:
return event

values = exception.get("values", None)

if values is None:
return event

for exception, (_exc_type, _exc_value, exc_tb) in zip(
reversed(values), walk_exception_chain(exc_info)
):
sentry_frames = [
frame
for frame in exception.get("stacktrace", {}).get("frames", [])
if frame.get("function")
]
tbs = list(iter_stacks(exc_tb))
if len(sentry_frames) != len(tbs):
continue

for sentry_frame, tb in zip(sentry_frames, tbs):
sentry_frame["vars"].update(pure_eval_frame(tb.tb_frame))
return event


def pure_eval_frame(frame):
# type: (FrameType) -> Dict[str, Any]
source = executing.Source.for_frame(frame)
if not source.tree:
return {}

statements = source.statements_at_line(frame.f_lineno)
if not statements:
return {}

stmt = list(statements)[0]
while True:
# Get the parent first in case the original statement is already
# a function definition, e.g. if we're calling a decorator
# In that case we still want the surrounding scope, not that function
stmt = stmt.parent
if isinstance(stmt, (ast.FunctionDef, ast.ClassDef, ast.Module)):
break

evaluator = pure_eval.Evaluator.from_frame(frame)
expressions = evaluator.interesting_expressions_grouped(stmt)
atok = source.asttokens()
return {atok.get_text(nodes[0]): value for nodes, value in expressions}
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ gevent
eventlet
newrelic
executing
asttokens
3 changes: 3 additions & 0 deletions tests/integrations/pure_eval/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest

pure_eval = pytest.importorskip("pure_eval")
35 changes: 35 additions & 0 deletions tests/integrations/pure_eval/test_pure_eval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pytest

from sentry_sdk import capture_exception
from sentry_sdk.integrations.pure_eval import PureEvalIntegration


@pytest.mark.parametrize("integrations", [[], [PureEvalIntegration()]])
def test_with_locals_enabled(sentry_init, capture_events, integrations):
sentry_init(with_locals=True, integrations=integrations)
events = capture_events()

def foo():
foo.d = {1: 2}
print(foo.d[1] / 0)

try:
foo()
except Exception:
capture_exception()

(event,) = events

assert all(
frame["vars"]
for frame in event["exception"]["values"][0]["stacktrace"]["frames"]
)

frame_vars = event["exception"]["values"][0]["stacktrace"]["frames"][-1]["vars"]

if integrations:
assert sorted(frame_vars.keys()) == ["foo", "foo.d", "foo.d[1]"]
assert frame_vars["foo.d"] == {"1": "2"}
assert frame_vars["foo.d[1]"] == "2"
else:
assert sorted(frame_vars.keys()) == ["foo"]
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ envlist =
[testenv]
deps =
-r test-requirements.txt

py3.{5,6,7,8}: pure_eval

django-{1.11,2.0,2.1,2.2,3.0,dev}: djangorestframework>=3.0.0,<4.0.0
{py3.7,py3.8}-django-{1.11,2.0,2.1,2.2,3.0,dev}: channels>2
Expand Down

0 comments on commit 2b8d96d

Please sign in to comment.